diff --git a/.gitignore b/.gitignore index 22969ab1..a1c7bea7 100644 --- a/.gitignore +++ b/.gitignore @@ -7,4 +7,5 @@ temp-testng* tomcat *.log velocity.log* - +.idea/ +classes/ diff --git a/src/main/java/scrum/client/dashboard/DashboardWidget.java b/src/main/java/scrum/client/dashboard/DashboardWidget.java index 05e86da3..2618b6cf 100644 --- a/src/main/java/scrum/client/dashboard/DashboardWidget.java +++ b/src/main/java/scrum/client/dashboard/DashboardWidget.java @@ -32,6 +32,11 @@ protected Widget onInitialization() { ScrumNavigatorWidget nav = widgets.getSidebar().getNavigator(); + PagePanel storyBurndown = new PagePanel(); + storyBurndown.addHeader("Story Burndown", + new HyperlinkWidget(nav.createSwitchAction(widgets.getSprintBacklog()))); + storyBurndown.addSection(new StoryBurndownWidget()); + PagePanel sprintBurndown = new PagePanel(); sprintBurndown.addHeader("Sprint Burndown", new HyperlinkWidget(nav.createSwitchAction(widgets.getSprintBacklog()))); @@ -67,7 +72,7 @@ protected Widget onInitialization() { events.addSection(new LatestEventsWidget()); Widget left = TableBuilder.column(5, sprintBurndown, teamsTasks, posTasks); - Widget right = TableBuilder.column(5, impediments, risks, events); + Widget right = TableBuilder.column(5, storyBurndown, impediments, risks, events); return TableBuilder.row(5, left, right); } diff --git a/src/main/java/scrum/client/dashboard/StoryBurndownWidget.java b/src/main/java/scrum/client/dashboard/StoryBurndownWidget.java new file mode 100644 index 00000000..aa77438a --- /dev/null +++ b/src/main/java/scrum/client/dashboard/StoryBurndownWidget.java @@ -0,0 +1,41 @@ +package scrum.client.dashboard; + +import ilarkesto.core.time.Tm; +import scrum.client.common.AScrumWidget; + +import com.google.gwt.user.client.Window; +import com.google.gwt.user.client.ui.Image; +import com.google.gwt.user.client.ui.Widget; + +public class StoryBurndownWidget extends AScrumWidget { + + public static final int CHART_WIDTH = 800; + public static final int CHART_HEIGHT = 270; + + private Image sprintChart; + + @Override + protected Widget onInitialization() { + sprintChart = new Image(getChartUrl(200)); + return sprintChart; + } + + @Override + protected void onUpdate() { + int width = getChartWidth(); + sprintChart.setWidth(width + "px"); + sprintChart.setUrl(getChartUrl(width) + "×tamp=" + Tm.getCurrentTimeMillis()); + } + + private String getChartUrl(int width) { + return getCurrentSprint().getStoryBurndown(width, CHART_HEIGHT); + } + + private int getChartWidth() { + int width = Window.getClientWidth() - 280; + width = width / 2; + if (width < 100) width = 100; + return width; + } + +} diff --git a/src/main/java/scrum/client/sprint/Sprint.java b/src/main/java/scrum/client/sprint/Sprint.java index 91786e8b..aab3264a 100644 --- a/src/main/java/scrum/client/sprint/Sprint.java +++ b/src/main/java/scrum/client/sprint/Sprint.java @@ -124,6 +124,11 @@ public String getChartUrl(int width, int height) { + height; } + public String getStoryBurndown(int width, int height) { + return GWT.getModuleBaseURL() + "storyBurndownChart.png?sprintId=" + getId() + "&width=" + width + "&height=" + + height; + } + public boolean isCompleted() { return getVelocity() != null; } diff --git a/src/main/java/scrum/server/ScrumWebApplication.java b/src/main/java/scrum/server/ScrumWebApplication.java index 5fd7dc2b..57f86d72 100644 --- a/src/main/java/scrum/server/ScrumWebApplication.java +++ b/src/main/java/scrum/server/ScrumWebApplication.java @@ -52,7 +52,8 @@ import scrum.server.admin.SystemConfig; import scrum.server.admin.User; import scrum.server.admin.UserDao; -import scrum.server.common.BurndownChart; +import scrum.server.common.TaskBurndownChart; +import scrum.server.common.StoryBurndownChart; import scrum.server.journal.ProjectEvent; import scrum.server.pr.EmailSender; import scrum.server.pr.SubscriptionService; @@ -66,7 +67,8 @@ public class ScrumWebApplication extends GScrumWebApplication { private static final Log log = Log.get(ScrumWebApplication.class); - private BurndownChart burndownChart; + private TaskBurndownChart burndownChart; + private StoryBurndownChart storyBurndownChart; private KunagiRootConfig config; private ScrumEntityfilePreparator entityfilePreparator; private SystemMessage systemMessage; @@ -86,14 +88,22 @@ protected int getDataVersion() { // --- composites --- - public BurndownChart getBurndownChart() { + public TaskBurndownChart getBurndownChart() { if (burndownChart == null) { - burndownChart = new BurndownChart(); + burndownChart = new TaskBurndownChart(); burndownChart.setSprintDao(getSprintDao()); } return burndownChart; } + public StoryBurndownChart getStoryBurndownChart() { + if (storyBurndownChart == null) { + storyBurndownChart = new StoryBurndownChart(); + storyBurndownChart.setSprintDao(getSprintDao()); + } + return storyBurndownChart; + } + public SystemConfig getSystemConfig() { return getSystemConfigDao().getSystemConfig(); } diff --git a/src/main/java/scrum/server/common/BurndownChart.java b/src/main/java/scrum/server/common/BurndownChart.java index ee9f8add..18c99b69 100644 --- a/src/main/java/scrum/server/common/BurndownChart.java +++ b/src/main/java/scrum/server/common/BurndownChart.java @@ -14,208 +14,27 @@ */ package scrum.server.common; -import ilarkesto.base.Str; -import ilarkesto.base.Sys; import ilarkesto.base.Utl; -import ilarkesto.core.logging.Log; -import ilarkesto.core.time.Date; -import java.awt.BasicStroke; import java.awt.Color; -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.io.OutputStream; -import java.text.NumberFormat; -import java.text.SimpleDateFormat; -import java.util.ArrayList; import java.util.List; -import java.util.Locale; -import org.jfree.chart.ChartFactory; -import org.jfree.chart.ChartUtilities; -import org.jfree.chart.JFreeChart; -import org.jfree.chart.axis.AxisLocation; -import org.jfree.chart.axis.DateAxis; -import org.jfree.chart.axis.DateTickUnit; -import org.jfree.chart.axis.NumberAxis; -import org.jfree.chart.axis.NumberTickUnit; -import org.jfree.chart.plot.PlotOrientation; -import org.jfree.chart.renderer.xy.XYItemRenderer; -import org.jfree.data.Range; -import org.jfree.data.xy.DefaultXYDataset; - -import scrum.client.common.WeekdaySelector; import scrum.server.css.ScreenCssBuilder; -import scrum.server.sprint.Sprint; import scrum.server.sprint.SprintDao; -import scrum.server.sprint.SprintDaySnapshot; - -public class BurndownChart { - - private static final Log LOG = Log.get(BurndownChart.class); - private static final Color COLOR_PAST_LINE = Utl.parseHtmlColor(ScreenCssBuilder.cBurndownLine); - private static final Color COLOR_PROJECTION_LINE = Utl.parseHtmlColor(ScreenCssBuilder.cBurndownProjectionLine); - private static final Color COLOR_OPTIMUM_LINE = Utl.parseHtmlColor(ScreenCssBuilder.cBurndownOptimalLine); +public abstract class BurndownChart { - // --- dependencies --- + protected static final Color COLOR_PAST_LINE = Utl.parseHtmlColor(ScreenCssBuilder.cBurndownLine); + protected static final Color COLOR_PROJECTION_LINE = Utl.parseHtmlColor(ScreenCssBuilder.cBurndownProjectionLine); + protected static final Color COLOR_OPTIMUM_LINE = Utl.parseHtmlColor(ScreenCssBuilder.cBurndownOptimalLine); - private SprintDao sprintDao; + protected SprintDao sprintDao; public void setSprintDao(SprintDao sprintDao) { this.sprintDao = sprintDao; } - // --- --- - - public static byte[] createBurndownChartAsByteArray(Sprint sprint, int width, int height) { - ByteArrayOutputStream out = new ByteArrayOutputStream(); - new BurndownChart().writeSprintBurndownChart(out, sprint, width, height); - return out.toByteArray(); - } - - public void writeSprintBurndownChart(OutputStream out, String sprintId, int width, int height) { - Sprint sprint = sprintDao.getById(sprintId); - if (sprint == null) throw new IllegalArgumentException("Sprint " + sprintId + " does not exist."); - writeSprintBurndownChart(out, sprint, width, height); - } - - public void writeSprintBurndownChart(OutputStream out, Sprint sprint, int width, int height) { - List snapshots = sprint.getDaySnapshots(); - if (snapshots.isEmpty()) { - Date date = Date.today(); - date = Date.latest(date, sprint.getBegin()); - date = Date.earliest(date, sprint.getEnd()); - sprint.getDaySnapshot(date).updateWithCurrentSprint(); - snapshots = sprint.getDaySnapshots(); - } - - WeekdaySelector freeDays = sprint.getProject().getFreeDaysAsWeekdaySelector(); - - writeSprintBurndownChart(out, snapshots, sprint.getBegin(), sprint.getEnd(), sprint.getOriginallyEnd(), - freeDays, width, height); - } - - static void writeSprintBurndownChart(OutputStream out, List snapshots, Date firstDay, - Date lastDay, Date originallyLastDay, WeekdaySelector freeDays, int width, int height) { - LOG.debug("Creating burndown chart:", snapshots.size(), "snapshots from", firstDay, "to", lastDay, "(" + width - + "x" + height + " px)"); - - int dayCount = firstDay.getPeriodTo(lastDay).toDays(); - int dateMarkTickUnit = 1; - float widthPerDay = (float) width / (float) dayCount * dateMarkTickUnit; - while (widthPerDay < 20) { - dateMarkTickUnit++; - widthPerDay = (float) width / (float) dayCount * dateMarkTickUnit; - } - - List burndownSnapshots = new ArrayList(snapshots); - JFreeChart chart = createSprintBurndownChart(burndownSnapshots, firstDay, lastDay, originallyLastDay, freeDays, - dateMarkTickUnit, widthPerDay); - try { - ChartUtilities.writeScaledChartAsPNG(out, chart, width, height, 1, 1); - out.flush(); - } catch (IOException e) { - throw new RuntimeException(e); - } - } - - private static JFreeChart createSprintBurndownChart(List snapshots, Date firstDay, Date lastDay, - Date originallyLastDay, WeekdaySelector freeDays, int dateMarkTickUnit, float widthPerDay) { - DefaultXYDataset data = createSprintBurndownChartDataset(snapshots, firstDay, lastDay, originallyLastDay, - freeDays); - - double tick = 1.0; - double max = BurndownChart.getMaximum(data); - - while (max / tick > 25) { - tick *= 2; - if (max / tick <= 25) break; - tick *= 2.5; - if (max / tick <= 25) break; - tick *= 2; - } - double valueLabelTickUnit = tick; - double upperBoundary = Math.min(max * 1.1f, max + 3); - - if (!Sys.isHeadless()) LOG.warn("GraphicsEnvironment is not headless"); - JFreeChart chart = ChartFactory.createXYLineChart("", "", "", data, PlotOrientation.VERTICAL, false, true, - false); - - XYItemRenderer renderer = chart.getXYPlot().getRenderer(); - - renderer.setSeriesPaint(0, COLOR_PAST_LINE); - renderer.setSeriesStroke(0, new BasicStroke(2.0f, BasicStroke.CAP_ROUND, BasicStroke.JOIN_BEVEL)); - renderer.setSeriesPaint(1, COLOR_PROJECTION_LINE); - renderer.setSeriesStroke(1, new BasicStroke(1.5f, BasicStroke.CAP_ROUND, BasicStroke.JOIN_BEVEL, 1.0f, - new float[] { 3f }, 0)); - renderer.setSeriesPaint(2, COLOR_OPTIMUM_LINE); - renderer.setSeriesStroke(2, new BasicStroke(2f, BasicStroke.CAP_ROUND, BasicStroke.JOIN_BEVEL)); - - DateAxis domainAxis1 = new DateAxis(); - String dateFormat = "d."; - widthPerDay -= 5; - if (widthPerDay > 40) { - dateFormat = "EE " + dateFormat; - } - if (widthPerDay > 10) { - float spaces = widthPerDay / 2.7f; - dateFormat = Str.multiply(" ", (int) spaces) + dateFormat; - } - domainAxis1.setDateFormatOverride(new SimpleDateFormat(dateFormat, Locale.US)); - domainAxis1.setTickUnit(new DateTickUnit(DateTickUnit.DAY, dateMarkTickUnit)); - domainAxis1.setAxisLineVisible(false); - Range range = new Range(firstDay.toMillis(), lastDay.nextDay().toMillis()); - domainAxis1.setRange(range); - - DateAxis domainAxis2 = new DateAxis(); - domainAxis2.setTickUnit(new DateTickUnit(DateTickUnit.DAY, 1)); - domainAxis2.setTickMarksVisible(false); - domainAxis2.setTickLabelsVisible(false); - domainAxis2.setRange(range); - - chart.getXYPlot().setDomainAxis(0, domainAxis2); - chart.getXYPlot().setDomainAxis(1, domainAxis1); - chart.getXYPlot().setDomainAxisLocation(1, AxisLocation.BOTTOM_OR_RIGHT); - - NumberAxis rangeAxis = new NumberAxis(); - rangeAxis.setNumberFormatOverride(NumberFormat.getIntegerInstance()); - rangeAxis.setTickUnit(new NumberTickUnit(valueLabelTickUnit)); - - rangeAxis.setLowerBound(0); - rangeAxis.setUpperBound(upperBoundary); - - chart.getXYPlot().setRangeAxis(rangeAxis); - - chart.getXYPlot().getRenderer().setBaseStroke(new BasicStroke(2f)); - - chart.setBackgroundPaint(Color.WHITE); - - return chart; - } - - static double getMaximum(DefaultXYDataset data) { - double max = 0; - for (int i = 0; i < data.getSeriesCount(); i++) { - for (int j = 0; j < data.getItemCount(i); j++) { - double value = data.getYValue(i, j); - if (value > max) { - max = value; - } - } - } - return max; - } - - static DefaultXYDataset createSprintBurndownChartDataset(final List snapshots, - final Date firstDay, final Date lastDay, Date originallyLastDay, final WeekdaySelector freeDays) { - - ChartDataFactory factory = new ChartDataFactory(); - factory.createDataset(snapshots, firstDay, lastDay, originallyLastDay, freeDays); - return factory.getDataset(); - } - - private static double[][] toArray(List a, List b) { + protected static double[][] toArray(List a, List b) { int min = Math.min(a.size(), b.size()); double[][] array = new double[2][min]; for (int i = 0; i < min; i++) { @@ -225,162 +44,4 @@ private static double[][] toArray(List a, List b) { return array; } - static class ChartDataFactory { - - List mainDates = new ArrayList(); - List mainValues = new ArrayList(); - - List extrapolationDates = new ArrayList(); - List extrapolationValues = new ArrayList(); - - List idealDates = new ArrayList(); - List idealValues = new ArrayList(); - - List snapshots; - WeekdaySelector freeDays; - - Date date; - long millisBegin; - long millisEnd; - boolean freeDay; - BurndownSnapshot snapshot; - boolean workStarted; - boolean workFinished; - - int totalBurned; - int totalBefore; - int totalAfter; - int burned; - int jump; - double totalRemaining; - int workDays; - double burnPerDay; - double idealRemaining; - double idealBurnPerDay; - int totalWorkDays = 0; - int totalOriginallyWorkDays = 0; - boolean extrapolationFinished; - - DefaultXYDataset dataset; - - public void createDataset(final List snapshots, final Date firstDay, final Date lastDay, - final Date originallyLastDay, final WeekdaySelector freeDays) { - this.snapshots = snapshots; - this.freeDays = freeDays; - - date = firstDay; - while (date.isBeforeOrSame(lastDay)) { - if (!freeDays.isFree(date.getWeekday().getDayOfWeek())) { - totalWorkDays++; - if (date.isBeforeOrSame(originallyLastDay)) totalOriginallyWorkDays++; - } - date = date.nextDay(); - } - - setDate(firstDay); - while (true) { - if (!workFinished) { - burned = snapshot.getBurnedWorkTotal() - totalBurned; - totalBurned = snapshot.getBurnedWorkTotal(); - totalAfter = snapshot.getRemainingWork(); - jump = totalAfter - totalBefore + burned; - } - - if (workFinished) { - processSuffix(); - } else if (workStarted) { - processCenter(); - } else { - processPrefix(); - } - if (date.equals(lastDay)) break; - setDate(date.nextDay()); - totalBefore = totalAfter; - } - - dataset = new DefaultXYDataset(); - dataset.addSeries("Main", toArray(mainDates, mainValues)); - dataset.addSeries("Extrapolation", toArray(extrapolationDates, extrapolationValues)); - dataset.addSeries("Ideal", toArray(idealDates, idealValues)); - } - - private void setDate(Date newDate) { - date = newDate; - millisBegin = date.toMillis(); - millisEnd = date.nextDay().toMillis(); - freeDay = freeDays.isFree(date.getWeekday().getDayOfWeek()); - if (!workFinished) snapshot = getSnapshot(); - } - - private void processPrefix() { - if (totalAfter > 0 || totalBurned > 0) { - workStarted = true; - idealRemaining = totalAfter + burned; - idealDates.add((double) millisBegin); - idealValues.add(idealRemaining); - - if (totalOriginallyWorkDays > 0) { - idealBurnPerDay = (double) jump / (double) totalOriginallyWorkDays; - } - - processCenter(); - return; - } - totalWorkDays--; - totalOriginallyWorkDays--; - } - - private void processCenter() { - mainDates.add((double) millisBegin); - mainValues.add((double) totalBefore); - if (jump != 0) { - mainDates.add((double) millisBegin); - mainValues.add((double) totalBefore + jump); - } - mainDates.add((double) millisEnd); - mainValues.add((double) totalAfter); - - if (!freeDay) { - workDays++; - idealRemaining -= idealBurnPerDay; - } - - if (idealRemaining > 0) { - idealDates.add((double) millisEnd); - idealValues.add(idealRemaining); - } - } - - private void processSuffix() { - if (!freeDay) { - totalRemaining -= burnPerDay; - idealRemaining -= idealBurnPerDay; - } - if (!extrapolationFinished) { - extrapolationDates.add((double) millisEnd); - extrapolationValues.add(totalRemaining); - } - idealDates.add((double) millisEnd); - idealValues.add(idealRemaining); - if (totalRemaining <= 0) extrapolationFinished = true; - } - - private BurndownSnapshot getSnapshot() { - for (BurndownSnapshot snapshot : snapshots) { - if (snapshot.getDate().equals(date)) return snapshot; - } - workFinished = true; - totalRemaining = totalAfter; - burnPerDay = (double) totalBurned / (double) workDays; - extrapolationDates.add((double) millisBegin); - extrapolationValues.add(totalRemaining); - return null; - } - - public DefaultXYDataset getDataset() { - return dataset; - } - - } - } diff --git a/src/main/java/scrum/server/common/DefaultXYDatasetUtil.java b/src/main/java/scrum/server/common/DefaultXYDatasetUtil.java new file mode 100644 index 00000000..80f49642 --- /dev/null +++ b/src/main/java/scrum/server/common/DefaultXYDatasetUtil.java @@ -0,0 +1,20 @@ +package scrum.server.common; + +import org.jfree.data.xy.DefaultXYDataset; + +public class DefaultXYDatasetUtil { + + static double getMaximum(DefaultXYDataset data) { + double max = 0; + for (int i = 0; i < data.getSeriesCount(); i++) { + for (int j = 0; j < data.getItemCount(i); j++) { + double value = data.getYValue(i, j); + if (value > max) { + max = value; + } + } + } + return max; + } + +} diff --git a/src/main/java/scrum/server/common/StoryBurndownChart.java b/src/main/java/scrum/server/common/StoryBurndownChart.java new file mode 100644 index 00000000..53431ce8 --- /dev/null +++ b/src/main/java/scrum/server/common/StoryBurndownChart.java @@ -0,0 +1,277 @@ +package scrum.server.common; + +import ilarkesto.base.Str; +import ilarkesto.base.Sys; +import ilarkesto.core.logging.Log; +import ilarkesto.core.time.Date; + +import java.awt.BasicStroke; +import java.awt.Color; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.text.NumberFormat; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; + +import org.jfree.chart.ChartFactory; +import org.jfree.chart.ChartUtilities; +import org.jfree.chart.JFreeChart; +import org.jfree.chart.axis.AxisLocation; +import org.jfree.chart.axis.DateAxis; +import org.jfree.chart.axis.DateTickUnit; +import org.jfree.chart.axis.NumberAxis; +import org.jfree.chart.axis.NumberTickUnit; +import org.jfree.chart.plot.PlotOrientation; +import org.jfree.chart.renderer.xy.XYItemRenderer; +import org.jfree.data.Range; +import org.jfree.data.xy.DefaultXYDataset; + +import scrum.client.common.WeekdaySelector; +import scrum.server.sprint.Sprint; +import scrum.server.sprint.SprintDaySnapshot; + +public class StoryBurndownChart extends BurndownChart { + + private static final Log LOG = Log.get(StoryBurndownChart.class); + + public void writeStoryBurndownChart(ByteArrayOutputStream out, String sprintId, int width, int height) { + Sprint sprint = sprintDao.getById(sprintId); + if (sprint == null) throw new IllegalArgumentException("Sprint " + sprintId + " does not exist."); + writeStoryBurndownChart(out, sprint, width, height); + } + + public void writeStoryBurndownChart(OutputStream out, Sprint sprint, int width, int height) { + List snapshots = sprint.getDaySnapshots(); + if (snapshots.isEmpty()) { + Date date = Date.today(); + date = Date.latest(date, sprint.getBegin()); + date = Date.earliest(date, sprint.getEnd()); + sprint.getDaySnapshot(date).updateWithCurrentSprint(); + snapshots = sprint.getDaySnapshots(); + } + + WeekdaySelector freeDays = sprint.getProject().getFreeDaysAsWeekdaySelector(); + + writeSprintBurndownChart(out, snapshots, sprint.getBegin(), sprint.getEnd(), sprint.getOriginallyEnd(), + freeDays, width, height, sprint); + } + + static void writeSprintBurndownChart(OutputStream out, List snapshots, Date firstDay, + Date lastDay, Date originallyLastDay, WeekdaySelector freeDays, int width, int height, Sprint sprint) { + LOG.debug("Creating burndown chart:", snapshots.size(), "snapshots from", firstDay, "to", lastDay, "(" + width + + "x" + height + " px)"); + + int dayCount = firstDay.getPeriodTo(lastDay).toDays(); + int dateMarkTickUnit = 1; + float widthPerDay = (float) width / (float) dayCount * dateMarkTickUnit; + while (widthPerDay < 20) { + dateMarkTickUnit++; + widthPerDay = (float) width / (float) dayCount * dateMarkTickUnit; + } + + List burndownSnapshots = new ArrayList(snapshots); + JFreeChart chart = createStoryBurndownChart(burndownSnapshots, firstDay, lastDay, originallyLastDay, freeDays, + dateMarkTickUnit, widthPerDay, sprint); + try { + ChartUtilities.writeScaledChartAsPNG(out, chart, width, height, 1, 1); + out.flush(); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + private static JFreeChart createStoryBurndownChart(List snapshots, Date firstDay, Date lastDay, + Date originallyLastDay, WeekdaySelector freeDays, int dateMarkTickUnit, float widthPerDay, Sprint sprint) { + DefaultXYDataset data = createStoryBurndownChartDataset(snapshots, firstDay, lastDay, originallyLastDay, + freeDays, sprint); + + double tick = 1.0; + double max = DefaultXYDatasetUtil.getMaximum(data); + + while (max / tick > 25) { + tick *= 2; + if (max / tick <= 25) break; + tick *= 2.5; + if (max / tick <= 25) break; + tick *= 2; + } + double valueLabelTickUnit = tick; + double upperBoundary = Math.min(max * 1.1f, max + 3); + + if (!Sys.isHeadless()) LOG.warn("GraphicsEnvironment is not headless"); + JFreeChart chart = ChartFactory.createXYLineChart("", "", "", data, PlotOrientation.VERTICAL, false, true, + false); + + XYItemRenderer renderer = chart.getXYPlot().getRenderer(); + + renderer.setSeriesPaint(0, COLOR_PAST_LINE); + renderer.setSeriesStroke(0, new BasicStroke(2.0f, BasicStroke.CAP_ROUND, BasicStroke.JOIN_BEVEL)); + renderer.setSeriesPaint(1, COLOR_PROJECTION_LINE); + renderer.setSeriesStroke(1, new BasicStroke(1.5f, BasicStroke.CAP_ROUND, BasicStroke.JOIN_BEVEL, 1.0f, + new float[] { 3f }, 0)); + renderer.setSeriesPaint(2, COLOR_OPTIMUM_LINE); + renderer.setSeriesStroke(2, new BasicStroke(2f, BasicStroke.CAP_ROUND, BasicStroke.JOIN_BEVEL)); + + DateAxis domainAxis1 = new DateAxis(); + String dateFormat = "d."; + widthPerDay -= 5; + if (widthPerDay > 40) { + dateFormat = "EE " + dateFormat; + } + if (widthPerDay > 10) { + float spaces = widthPerDay / 2.7f; + dateFormat = Str.multiply(" ", (int) spaces) + dateFormat; + } + domainAxis1.setDateFormatOverride(new SimpleDateFormat(dateFormat, Locale.US)); + domainAxis1.setTickUnit(new DateTickUnit(DateTickUnit.DAY, dateMarkTickUnit)); + domainAxis1.setAxisLineVisible(false); + Range range = new Range(firstDay.toMillis(), lastDay.nextDay().toMillis()); + domainAxis1.setRange(range); + + DateAxis domainAxis2 = new DateAxis(); + domainAxis2.setTickUnit(new DateTickUnit(DateTickUnit.DAY, 1)); + domainAxis2.setTickMarksVisible(false); + domainAxis2.setTickLabelsVisible(false); + domainAxis2.setRange(range); + + chart.getXYPlot().setDomainAxis(0, domainAxis2); + chart.getXYPlot().setDomainAxis(1, domainAxis1); + chart.getXYPlot().setDomainAxisLocation(1, AxisLocation.BOTTOM_OR_RIGHT); + + NumberAxis rangeAxis = new NumberAxis(); + rangeAxis.setNumberFormatOverride(NumberFormat.getIntegerInstance()); + rangeAxis.setTickUnit(new NumberTickUnit(valueLabelTickUnit)); + + rangeAxis.setLowerBound(0); + rangeAxis.setUpperBound(upperBoundary); + + chart.getXYPlot().setRangeAxis(rangeAxis); + + chart.getXYPlot().getRenderer().setBaseStroke(new BasicStroke(2f)); + + chart.setBackgroundPaint(Color.WHITE); + + return chart; + } + + static DefaultXYDataset createStoryBurndownChartDataset(final List snapshots, + final Date firstDay, final Date lastDay, Date originallyLastDay, final WeekdaySelector freeDays, + Sprint sprint) { + return new ChartDataFactory().createDataset(snapshots, firstDay, lastDay, originallyLastDay, freeDays, sprint); + } + + static class ChartDataFactory { + + List dateLine = new ArrayList(); + List totalStoriesLine = new ArrayList(); + List openStoriesLine = new ArrayList(); + List expectedStoriesLine = new ArrayList(); + + List snapshots; + WeekdaySelector freeDays; + + Date date; + long millisBegin; + long millisEnd; + boolean freeDay; + SprintDaySnapshot snapshot; + + double expectedStories = -1; + int openStories = 0; + int totalStories = 0; + + int totalWorkDays = 0; + int workDays; + + public DefaultXYDataset createDataset(final List snapshots, final Date firstDay, + final Date lastDay, final Date originallyLastDay, final WeekdaySelector freeDays, Sprint sprint) { + this.snapshots = snapshots; + this.freeDays = freeDays; + + date = firstDay; + while (date.isBeforeOrSame(lastDay)) { + if (!freeDays.isFree(date.getWeekday().getDayOfWeek())) { + totalWorkDays++; + } + date = date.nextDay(); + } + + setDate(firstDay); + while (true) { + if (date.isPastOrToday()) { + calculateOnRealData(); + } else { + calculateForFuture(); + } + + if (date.equals(lastDay)) break; + setDate(date.nextDay()); + } + + DefaultXYDataset dataset = new DefaultXYDataset(); + dataset.addSeries("Open", toArray(dateLine, openStoriesLine)); + dataset.addSeries("Ideal", toArray(dateLine, expectedStoriesLine)); + dataset.addSeries("Total", toArray(dateLine, totalStoriesLine)); + return dataset; + } + + private void calculateForFuture() { + dateLine.add((double) millisBegin); + dateLine.add((double) millisEnd); + + totalStoriesLine.add((double) totalStories); + totalStoriesLine.add((double) totalStories); + + double diff = (double) openStories / (double) (totalWorkDays - workDays); + + if (expectedStories == -1) { + expectedStories = openStories; + } + + expectedStoriesLine.add(expectedStories); + if (!freeDay) expectedStories -= diff; + expectedStoriesLine.add(expectedStories); + } + + private void calculateOnRealData() { + totalStories = snapshot.getTotalStories(); + openStories = totalStories - snapshot.getClosedStories(); + + dateLine.add((double) millisBegin); + dateLine.add((double) millisEnd); + + totalStoriesLine.add((double) totalStories); + totalStoriesLine.add((double) totalStories); + + openStoriesLine.add((double) openStories); + openStoriesLine.add((double) openStories); + + expectedStoriesLine.add((double) openStories); + expectedStoriesLine.add((double) openStories); + + if (!freeDay) { + workDays++; + } + } + + private void setDate(Date newDate) { + date = newDate; + millisBegin = date.toMillis(); + millisEnd = date.nextDay().toMillis(); + freeDay = freeDays.isFree(date.getWeekday().getDayOfWeek()); + snapshot = getSnapshot(date); + } + + private SprintDaySnapshot getSnapshot(Date date) { + for (SprintDaySnapshot snapshot : snapshots) { + if (snapshot.getDate().equals(date)) return snapshot; + } + return null; + } + + } + +} diff --git a/src/main/java/scrum/server/common/TaskBurndownChart.java b/src/main/java/scrum/server/common/TaskBurndownChart.java new file mode 100644 index 00000000..441e84df --- /dev/null +++ b/src/main/java/scrum/server/common/TaskBurndownChart.java @@ -0,0 +1,346 @@ +/* + * Copyright 2009, 2010, 2011 Fabian Hager, Witoslaw Koczewsi + * + * This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero + * General Public License as published by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the + * implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU General Public License along with this program. If not, see + * . + */ +package scrum.server.common; + +import ilarkesto.base.Str; +import ilarkesto.base.Sys; +import ilarkesto.core.logging.Log; +import ilarkesto.core.time.Date; + +import java.awt.BasicStroke; +import java.awt.Color; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.text.NumberFormat; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; + +import org.jfree.chart.ChartFactory; +import org.jfree.chart.ChartUtilities; +import org.jfree.chart.JFreeChart; +import org.jfree.chart.axis.AxisLocation; +import org.jfree.chart.axis.DateAxis; +import org.jfree.chart.axis.DateTickUnit; +import org.jfree.chart.axis.NumberAxis; +import org.jfree.chart.axis.NumberTickUnit; +import org.jfree.chart.plot.PlotOrientation; +import org.jfree.chart.renderer.xy.XYItemRenderer; +import org.jfree.data.Range; +import org.jfree.data.xy.DefaultXYDataset; + +import scrum.client.common.WeekdaySelector; +import scrum.server.sprint.Sprint; +import scrum.server.sprint.SprintDaySnapshot; + +public class TaskBurndownChart extends BurndownChart { + + private static final Log LOG = Log.get(TaskBurndownChart.class); + + public static byte[] createBurndownChartAsByteArray(Sprint sprint, int width, int height) { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + new TaskBurndownChart().writeSprintBurndownChart(out, sprint, width, height); + return out.toByteArray(); + } + + public void writeSprintBurndownChart(OutputStream out, String sprintId, int width, int height) { + Sprint sprint = sprintDao.getById(sprintId); + if (sprint == null) throw new IllegalArgumentException("Sprint " + sprintId + " does not exist."); + writeSprintBurndownChart(out, sprint, width, height); + } + + public void writeSprintBurndownChart(OutputStream out, Sprint sprint, int width, int height) { + List snapshots = sprint.getDaySnapshots(); + if (snapshots.isEmpty()) { + Date date = Date.today(); + date = Date.latest(date, sprint.getBegin()); + date = Date.earliest(date, sprint.getEnd()); + sprint.getDaySnapshot(date).updateWithCurrentSprint(); + snapshots = sprint.getDaySnapshots(); + } + + WeekdaySelector freeDays = sprint.getProject().getFreeDaysAsWeekdaySelector(); + + writeSprintBurndownChart(out, snapshots, sprint.getBegin(), sprint.getEnd(), sprint.getOriginallyEnd(), + freeDays, width, height); + } + + static void writeSprintBurndownChart(OutputStream out, List snapshots, Date firstDay, + Date lastDay, Date originallyLastDay, WeekdaySelector freeDays, int width, int height) { + LOG.debug("Creating burndown chart:", snapshots.size(), "snapshots from", firstDay, "to", lastDay, "(" + width + + "x" + height + " px)"); + + int dayCount = firstDay.getPeriodTo(lastDay).toDays(); + int dateMarkTickUnit = 1; + float widthPerDay = (float) width / (float) dayCount * dateMarkTickUnit; + while (widthPerDay < 20) { + dateMarkTickUnit++; + widthPerDay = (float) width / (float) dayCount * dateMarkTickUnit; + } + + List burndownSnapshots = new ArrayList(snapshots); + JFreeChart chart = createSprintBurndownChart(burndownSnapshots, firstDay, lastDay, originallyLastDay, freeDays, + dateMarkTickUnit, widthPerDay); + try { + ChartUtilities.writeScaledChartAsPNG(out, chart, width, height, 1, 1); + out.flush(); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + private static JFreeChart createSprintBurndownChart(List snapshots, Date firstDay, Date lastDay, + Date originallyLastDay, WeekdaySelector freeDays, int dateMarkTickUnit, float widthPerDay) { + DefaultXYDataset data = createSprintBurndownChartDataset(snapshots, firstDay, lastDay, originallyLastDay, + freeDays); + + double tick = 1.0; + double max = DefaultXYDatasetUtil.getMaximum(data); + + while (max / tick > 25) { + tick *= 2; + if (max / tick <= 25) break; + tick *= 2.5; + if (max / tick <= 25) break; + tick *= 2; + } + double valueLabelTickUnit = tick; + double upperBoundary = Math.min(max * 1.1f, max + 3); + + if (!Sys.isHeadless()) LOG.warn("GraphicsEnvironment is not headless"); + JFreeChart chart = ChartFactory.createXYLineChart("", "", "", data, PlotOrientation.VERTICAL, false, true, + false); + + XYItemRenderer renderer = chart.getXYPlot().getRenderer(); + + renderer.setSeriesPaint(0, COLOR_PAST_LINE); + renderer.setSeriesStroke(0, new BasicStroke(2.0f, BasicStroke.CAP_ROUND, BasicStroke.JOIN_BEVEL)); + renderer.setSeriesPaint(1, COLOR_PROJECTION_LINE); + renderer.setSeriesStroke(1, new BasicStroke(1.5f, BasicStroke.CAP_ROUND, BasicStroke.JOIN_BEVEL, 1.0f, + new float[] { 3f }, 0)); + renderer.setSeriesPaint(2, COLOR_OPTIMUM_LINE); + renderer.setSeriesStroke(2, new BasicStroke(2f, BasicStroke.CAP_ROUND, BasicStroke.JOIN_BEVEL)); + + DateAxis domainAxis1 = new DateAxis(); + String dateFormat = "d."; + widthPerDay -= 5; + if (widthPerDay > 40) { + dateFormat = "EE " + dateFormat; + } + if (widthPerDay > 10) { + float spaces = widthPerDay / 2.7f; + dateFormat = Str.multiply(" ", (int) spaces) + dateFormat; + } + domainAxis1.setDateFormatOverride(new SimpleDateFormat(dateFormat, Locale.US)); + domainAxis1.setTickUnit(new DateTickUnit(DateTickUnit.DAY, dateMarkTickUnit)); + domainAxis1.setAxisLineVisible(false); + Range range = new Range(firstDay.toMillis(), lastDay.nextDay().toMillis()); + domainAxis1.setRange(range); + + DateAxis domainAxis2 = new DateAxis(); + domainAxis2.setTickUnit(new DateTickUnit(DateTickUnit.DAY, 1)); + domainAxis2.setTickMarksVisible(false); + domainAxis2.setTickLabelsVisible(false); + domainAxis2.setRange(range); + + chart.getXYPlot().setDomainAxis(0, domainAxis2); + chart.getXYPlot().setDomainAxis(1, domainAxis1); + chart.getXYPlot().setDomainAxisLocation(1, AxisLocation.BOTTOM_OR_RIGHT); + + NumberAxis rangeAxis = new NumberAxis(); + rangeAxis.setNumberFormatOverride(NumberFormat.getIntegerInstance()); + rangeAxis.setTickUnit(new NumberTickUnit(valueLabelTickUnit)); + + rangeAxis.setLowerBound(0); + rangeAxis.setUpperBound(upperBoundary); + + chart.getXYPlot().setRangeAxis(rangeAxis); + + chart.getXYPlot().getRenderer().setBaseStroke(new BasicStroke(2f)); + + chart.setBackgroundPaint(Color.WHITE); + + return chart; + } + + static DefaultXYDataset createSprintBurndownChartDataset(final List snapshots, + final Date firstDay, final Date lastDay, Date originallyLastDay, final WeekdaySelector freeDays) { + + ChartDataFactory factory = new ChartDataFactory(); + factory.createDataset(snapshots, firstDay, lastDay, originallyLastDay, freeDays); + return factory.getDataset(); + } + + static class ChartDataFactory { + + List mainDates = new ArrayList(); + List mainValues = new ArrayList(); + + List extrapolationDates = new ArrayList(); + List extrapolationValues = new ArrayList(); + + List idealDates = new ArrayList(); + List idealValues = new ArrayList(); + + List snapshots; + WeekdaySelector freeDays; + + Date date; + long millisBegin; + long millisEnd; + boolean freeDay; + BurndownSnapshot snapshot; + boolean workStarted; + boolean workFinished; + + int totalBurned; + int totalBefore; + int totalAfter; + int burned; + int jump; + double totalRemaining; + int workDays; + double burnPerDay; + double idealRemaining; + double idealBurnPerDay; + int totalWorkDays = 0; + int totalOriginallyWorkDays = 0; + boolean extrapolationFinished; + + DefaultXYDataset dataset; + + public void createDataset(final List snapshots, final Date firstDay, final Date lastDay, + final Date originallyLastDay, final WeekdaySelector freeDays) { + this.snapshots = snapshots; + this.freeDays = freeDays; + + date = firstDay; + while (date.isBeforeOrSame(lastDay)) { + if (!freeDays.isFree(date.getWeekday().getDayOfWeek())) { + totalWorkDays++; + if (date.isBeforeOrSame(originallyLastDay)) totalOriginallyWorkDays++; + } + date = date.nextDay(); + } + + setDate(firstDay); + while (true) { + if (!workFinished) { + burned = snapshot.getBurnedWorkTotal() - totalBurned; + totalBurned = snapshot.getBurnedWorkTotal(); + totalAfter = snapshot.getRemainingWork(); + jump = totalAfter - totalBefore + burned; + } + + if (workFinished) { + processSuffix(); + } else if (workStarted) { + processCenter(); + } else { + processPrefix(); + } + if (date.equals(lastDay)) break; + setDate(date.nextDay()); + totalBefore = totalAfter; + } + + dataset = new DefaultXYDataset(); + dataset.addSeries("Main", toArray(mainDates, mainValues)); + dataset.addSeries("Extrapolation", toArray(extrapolationDates, extrapolationValues)); + dataset.addSeries("Ideal", toArray(idealDates, idealValues)); + } + + private void setDate(Date newDate) { + date = newDate; + millisBegin = date.toMillis(); + millisEnd = date.nextDay().toMillis(); + freeDay = freeDays.isFree(date.getWeekday().getDayOfWeek()); + if (!workFinished) snapshot = getSnapshot(); + } + + private void processPrefix() { + if (totalAfter > 0 || totalBurned > 0) { + workStarted = true; + idealRemaining = totalAfter + burned; + idealDates.add((double) millisBegin); + idealValues.add(idealRemaining); + + if (totalOriginallyWorkDays > 0) { + idealBurnPerDay = (double) jump / (double) totalOriginallyWorkDays; + } + + processCenter(); + return; + } + totalWorkDays--; + totalOriginallyWorkDays--; + } + + private void processCenter() { + mainDates.add((double) millisBegin); + mainValues.add((double) totalBefore); + if (jump != 0) { + mainDates.add((double) millisBegin); + mainValues.add((double) totalBefore + jump); + } + mainDates.add((double) millisEnd); + mainValues.add((double) totalAfter); + + if (!freeDay) { + workDays++; + idealRemaining -= idealBurnPerDay; + } + + if (idealRemaining > 0) { + idealDates.add((double) millisEnd); + idealValues.add(idealRemaining); + } + } + + private void processSuffix() { + if (!freeDay) { + totalRemaining -= burnPerDay; + idealRemaining -= idealBurnPerDay; + } + if (!extrapolationFinished) { + extrapolationDates.add((double) millisEnd); + extrapolationValues.add(totalRemaining); + } + idealDates.add((double) millisEnd); + idealValues.add(idealRemaining); + if (totalRemaining <= 0) extrapolationFinished = true; + } + + private BurndownSnapshot getSnapshot() { + for (BurndownSnapshot snapshot : snapshots) { + if (snapshot.getDate().equals(date)) return snapshot; + } + workFinished = true; + totalRemaining = totalAfter; + burnPerDay = (double) totalBurned / (double) workDays; + extrapolationDates.add((double) millisBegin); + extrapolationValues.add(totalRemaining); + return null; + } + + public DefaultXYDataset getDataset() { + return dataset; + } + + } + +} diff --git a/src/main/java/scrum/server/project/HomepageUpdater.java b/src/main/java/scrum/server/project/HomepageUpdater.java index fed34fe0..0d96ebfa 100644 --- a/src/main/java/scrum/server/project/HomepageUpdater.java +++ b/src/main/java/scrum/server/project/HomepageUpdater.java @@ -45,7 +45,7 @@ import scrum.server.collaboration.Comment; import scrum.server.collaboration.CommentDao; import scrum.server.collaboration.Wikipage; -import scrum.server.common.BurndownChart; +import scrum.server.common.TaskBurndownChart; import scrum.server.issues.Issue; import scrum.server.pr.BlogEntry; import scrum.server.release.Release; @@ -230,7 +230,7 @@ private void processBlogEntryTemplate(BlogEntry entry) { } private void createSprintBurndownChart(int width, int height) { - byte[] imageData = BurndownChart.createBurndownChartAsByteArray(project.getCurrentSprint(), width, height); + byte[] imageData = TaskBurndownChart.createBurndownChartAsByteArray(project.getCurrentSprint(), width, height); IO.copyDataToFile(imageData, new File(outputDir.getPath() + "/sprint-burndown-" + width + "x" + height + ".png")); } diff --git a/src/main/java/scrum/server/sprint/SprintBacklogPdfCreator.java b/src/main/java/scrum/server/sprint/SprintBacklogPdfCreator.java index c7a3e195..10fbc1a3 100644 --- a/src/main/java/scrum/server/sprint/SprintBacklogPdfCreator.java +++ b/src/main/java/scrum/server/sprint/SprintBacklogPdfCreator.java @@ -23,7 +23,7 @@ import java.util.List; import scrum.server.common.APdfCreator; -import scrum.server.common.BurndownChart; +import scrum.server.common.TaskBurndownChart; import scrum.server.project.Project; import scrum.server.project.Requirement; @@ -56,7 +56,7 @@ protected void build(APdfContainerElement pdf) { fields.field("Team").text(sprint.getTeamMembersAsString()); pdf.nl(); - pdf.image(BurndownChart.createBurndownChartAsByteArray(sprint, 1000, 500)).setScaleByWidth(150f); + pdf.image(TaskBurndownChart.createBurndownChartAsByteArray(sprint, 1000, 500)).setScaleByWidth(150f); pdf.nl(); List requirements = new ArrayList(sprint.getRequirements()); diff --git a/src/main/java/scrum/server/sprint/SprintDaySnapshot.java b/src/main/java/scrum/server/sprint/SprintDaySnapshot.java index 68ac1e17..6aa54dec 100644 --- a/src/main/java/scrum/server/sprint/SprintDaySnapshot.java +++ b/src/main/java/scrum/server/sprint/SprintDaySnapshot.java @@ -20,6 +20,17 @@ public class SprintDaySnapshot extends GSprintDaySnapshot implements BurndownSnapshot { + public int getClosedStories() { + return closedStories; + } + + public int getTotalStories() { + return totalStories; + } + + private int closedStories; + private int totalStories; + public void addBurnedWorkFromDeleted(int work) { setBurnedWorkFromDeleted(getBurnedWorkFromDeleted() + work); } @@ -48,4 +59,18 @@ public String toString() { return getDate() + ": " + getBurnedWorkTotal() + ", " + getRemainingWork(); } + public void setClosedStories(int closedStories) { + if (closedStories == this.closedStories) return; + this.closedStories = closedStories; + updateLastModified(); + fireModified("closedStories=" + closedStories); + } + + public void setTotalStories(int totalStories) { + if (totalStories == this.totalStories) return; + this.totalStories = totalStories; + updateLastModified(); + fireModified("totalStories=" + totalStories); + } + } diff --git a/src/main/java/scrum/server/sprint/SprintDaySnapshotDao.java b/src/main/java/scrum/server/sprint/SprintDaySnapshotDao.java index 2ea72488..f3f09419 100644 --- a/src/main/java/scrum/server/sprint/SprintDaySnapshotDao.java +++ b/src/main/java/scrum/server/sprint/SprintDaySnapshotDao.java @@ -19,6 +19,9 @@ import java.util.LinkedList; import java.util.List; +import java.util.Set; + +import scrum.server.project.Requirement; public class SprintDaySnapshotDao extends GSprintDaySnapshotDao { @@ -31,16 +34,39 @@ public boolean test(SprintDaySnapshot e) { } }); + if (snapshot != null && snapshot.getDate().isToday()) { + updateStoryCount(sprint, snapshot); + saveEntity(snapshot); + } + if (autoCreate && snapshot == null) { snapshot = newEntityInstance(); snapshot.setSprint(sprint); snapshot.setDate(date); + + updateStoryCount(sprint, snapshot); + saveEntity(snapshot); } return snapshot; } + private void updateStoryCount(final Sprint sprint, SprintDaySnapshot snapshot) { + Set requirements = sprint.getRequirements(); + + int closedStories = 0; + for (Requirement req : requirements) { + if (req.isClosed()) { + closedStories++; + } + } + snapshot.setClosedStories(closedStories); + + int totalStories = requirements.size(); + snapshot.setTotalStories(totalStories); + } + public List getSprintDaySnapshots(Sprint sprint) { List ret = new LinkedList(); Date date = sprint.getBegin(); @@ -57,6 +83,7 @@ public List getSprintDaySnapshots(Sprint sprint) { snapshot.setBurnedWork(previousSnapshot.getBurnedWork()); } } + ret.add(snapshot); previousSnapshot = snapshot; date = date.nextDay(); diff --git a/src/main/java/scrum/server/sprint/SprintReportPdfCreator.java b/src/main/java/scrum/server/sprint/SprintReportPdfCreator.java index 75994588..9aafa550 100644 --- a/src/main/java/scrum/server/sprint/SprintReportPdfCreator.java +++ b/src/main/java/scrum/server/sprint/SprintReportPdfCreator.java @@ -30,7 +30,7 @@ import scrum.client.sprint.SprintHistoryHelper.StoryInfo; import scrum.client.sprint.SprintHistoryHelper.TaskInfo; import scrum.server.common.APdfCreator; -import scrum.server.common.BurndownChart; +import scrum.server.common.TaskBurndownChart; import scrum.server.common.ScrumPdfContext; import scrum.server.common.WikiToPdfConverter; import scrum.server.project.Requirement; @@ -66,7 +66,7 @@ protected void build(APdfContainerElement pdf) { fields.field("Team").text(sprint.getTeamMembersAsString()); pdf.nl(); - pdf.image(BurndownChart.createBurndownChartAsByteArray(sprint, 1000, 500)).setScaleByWidth(150f); + pdf.image(TaskBurndownChart.createBurndownChartAsByteArray(sprint, 1000, 500)).setScaleByWidth(150f); ScrumPdfContext pdfContext = new ScrumPdfContext(sprint.getProject()); if (sprint.isGoalSet()) { diff --git a/src/main/java/scrum/server/sprint/StoryBurndownChartServlet.java b/src/main/java/scrum/server/sprint/StoryBurndownChartServlet.java new file mode 100644 index 00000000..4bdb221a --- /dev/null +++ b/src/main/java/scrum/server/sprint/StoryBurndownChartServlet.java @@ -0,0 +1,34 @@ +package scrum.server.sprint; + +import ilarkesto.webapp.RequestWrapper; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; + +import scrum.client.project.ProjectOverviewWidget; +import scrum.server.ScrumWebApplication; +import scrum.server.WebSession; +import scrum.server.common.AKunagiServlet; + +public class StoryBurndownChartServlet extends AKunagiServlet { + + @Override + protected void onRequest(RequestWrapper req) throws IOException { + String sprintId = req.get("sprintId"); + String width = req.get("width"); + if (width == null) width = String.valueOf(ProjectOverviewWidget.CHART_WIDTH); + String height = req.get("height"); + if (height == null) height = String.valueOf(ProjectOverviewWidget.CHART_HEIGHT); + + req.preventCaching(); + req.setContentType("image/png"); + + ByteArrayOutputStream out = new ByteArrayOutputStream(); + + ScrumWebApplication.get().getStoryBurndownChart() + .writeStoryBurndownChart(out, sprintId, Integer.parseInt(width), Integer.parseInt(height)); + + req.write(out.toByteArray()); + } + +} diff --git a/src/main/webapp/WEB-INF/web.xml b/src/main/webapp/WEB-INF/web.xml index b6a5b9f0..01190211 100755 --- a/src/main/webapp/WEB-INF/web.xml +++ b/src/main/webapp/WEB-INF/web.xml @@ -153,6 +153,17 @@ /scrum.ScrumGwtApplication/sprintBurndownChart.png + + + storyBurndownChart + scrum.server.sprint.StoryBurndownChartServlet + 2 + + + storyBurndownChart + /scrum.ScrumGwtApplication/storyBurndownChart.png + + pdf diff --git a/src/test/java/scrum/server/common/BurndownChartTest.java b/src/test/java/scrum/server/common/BurndownChartTest.java index 42d92cde..e4ff84db 100644 --- a/src/test/java/scrum/server/common/BurndownChartTest.java +++ b/src/test/java/scrum/server/common/BurndownChartTest.java @@ -81,7 +81,7 @@ public void sprintBurndown() throws IOException { log.info(file.getAbsolutePath()); IO.createDirectory(file.getParentFile()); BufferedOutputStream out = new BufferedOutputStream(new FileOutputStream(file)); - BurndownChart.writeSprintBurndownChart(out, shots, sprintBeginDate, sprintEndDate, sprintOriginallyEndDate, + TaskBurndownChart.writeSprintBurndownChart(out, shots, sprintBeginDate, sprintEndDate, sprintOriginallyEndDate, freeDays, 1000, 500); out.close(); }