///////////////////////////////////////////////////////////////////////////// // // Project ProjectForge Community Edition // www.projectforge.org // // Copyright (C) 2001-2014 Kai Reinhard (k.reinhard@micromata.de) // // ProjectForge is dual-licensed. // // This community edition is free software; you can redistribute it and/or // modify it under the terms of the GNU General Public License as published // by the Free Software Foundation; version 3 of the License. // // This community edition 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 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 http://www.gnu.org/licenses/. // ///////////////////////////////////////////////////////////////////////////// package org.projectforge.gantt; import java.util.ArrayList; import java.util.Calendar; import java.util.Collection; import java.util.Date; import java.util.HashMap; import java.util.Map; import org.apache.commons.collections.CollectionUtils; import org.apache.commons.lang.StringUtils; import org.projectforge.common.DateHolder; import org.projectforge.export.SVGColor; import org.projectforge.export.SVGHelper; import org.projectforge.export.SVGHelper.ArrowDirection; import org.projectforge.xml.stream.XmlObject; import org.w3c.dom.Document; import org.w3c.dom.Element; @XmlObject(alias = "ganttChart") public class GanttChart { private static final org.apache.log4j.Logger log = org.apache.log4j.Logger.getLogger(GanttChart.class); @SuppressWarnings("unused") private String name; private GanttChartStyle style; private GanttChartSettings settings; private Date fromDate; private Date toDate; private transient Date calculatedStartDate; private transient Date calculatedEndDate; private transient int fromToDays = -1; private transient double height; private GanttTask rootNode; private transient String fontFamily = "Helvetica"; private transient Map<GanttTask, ObjectInfo> objectMap = new HashMap<GanttTask, ObjectInfo>(); private class ObjectInfo { final Date fromDate; final Date toDate; final double x1; final double x2; final int row; final double y; ObjectInfo(final GanttTask node, final int row) { this.fromDate = GanttUtils.getCalculatedStartDate(node); this.toDate = GanttUtils.getCalculatedEndDate(node); if (fromDate != null) { this.x1 = getXValue(fromDate); } else { x1 = 0; } if (toDate != null) { this.x2 = getXValue(toDate); } else { x2 = 0; } this.row = row; this.y = style.getYScale() * row; } boolean isNaN() { return fromDate == null || toDate == null; } boolean isVisible() { return this.row >= 0; } } public GanttChart() { this.style = new GanttChartStyle(); this.settings = new GanttChartSettings(); } public GanttChart(final GanttTask rootNode, final GanttChartStyle style, final GanttChartSettings settings, final String name) { this.rootNode = rootNode; this.style = style; this.settings = settings; this.name = name; } public GanttChart setFontFamily(String fontFamily) { this.fontFamily = fontFamily; return this; } private ObjectInfo getObjectInfo(final GanttTask node) { ObjectInfo taskInfo = objectMap.get(node); if (taskInfo != null) { return taskInfo; } taskInfo = new ObjectInfo(node, -1); objectMap.put(node, taskInfo); return taskInfo; } public int getWidth() { return style.getWidth(); } /** * The earliest date of all contained tasks. */ public Date getCalculatedStartDate() { return calculatedStartDate; } /** * The latest date of all contained tasks. */ public Date getCalculatedEndDate() { return calculatedEndDate; } public GanttTask getRootNode() { return rootNode; } /** * Usage: * * <pre> * final BatikImage ganttImage = new BatikImage("ganttTest", ganttDiagram.create(), 800); * body.add(ganttImage); * </pre> * @return The SVG DOM model for this Gantt diagram. */ public Document create() { if (rootNode == null || rootNode.getChildren() == null) { return null; } int row = 0; final Collection<GanttTask> allVisibleGanttObjects = recalculate(); if (settings.getFromDate() != null) { fromDate = settings.getFromDate(); } if (settings.getToDate() != null) { toDate = settings.getToDate(); } if (fromDate == null) { fromDate = new DateHolder().setBeginOfDay().setHourOfDay(8).getDate(); } if (toDate == null) { toDate = new DateHolder().setBeginOfDay().setHourOfDay(8).add(Calendar.DAY_OF_MONTH, 30).getDate(); } for (final GanttTask node : allVisibleGanttObjects) { final ObjectInfo taskInfo = new ObjectInfo(node, row++); objectMap.put(node, taskInfo); } height = style.getYScale() * row + GanttChartStyle.HEAD_HEIGHT; final Document doc = SVGHelper.createDocument(style.getWidth(), height); final Element root = doc.getDocumentElement(); Element e, g1, g2, g3; if (getDiagramWidth() < 0) { g1 = SVGHelper.createElement(doc, "g", "font-size", "9pt"); root.appendChild(g1); g1.appendChild(SVGHelper.createText(doc, 0, 0, "TO SMALL")); return doc; } // Defs e = SVGHelper.createElement(doc, "defs"); root.appendChild(e); e.appendChild(SVGHelper.createElement(doc, "path", SVGColor.DARK_RED, "d", "M 0 0 L " + GanttChartStyle.SUMMARY_ARROW_SIZE + " 0 L 0 " + GanttChartStyle.SUMMARY_ARROW_SIZE + " z", "id", "redLeftArrow")); e.appendChild(SVGHelper.createElement(doc, "path", SVGColor.DARK_RED, "d", "M 0 0 L " + GanttChartStyle.SUMMARY_ARROW_SIZE + " 0 L " + GanttChartStyle.SUMMARY_ARROW_SIZE + " " + GanttChartStyle.SUMMARY_ARROW_SIZE + " z", "id", "redRightArrow")); e.appendChild(SVGHelper.createElement(doc, "path", SVGColor.BLACK, "d", "M -5 0 L 0 5 L 5 0 L 0 -5 z", "id", "diamond")); e = SVGHelper.createElement(doc, "defs"); root.appendChild(e); g1 = SVGHelper.createElement(doc, "g", "transform", "translate(5,20)"); root.appendChild(g1); if (fontFamily != null) { g2 = SVGHelper.createElement(doc, "g", "font-family", fontFamily, "font-size", "9pt"); } else { g2 = SVGHelper.createElement(doc, "g", "font-size", "9pt"); } g1.appendChild(g2); if (style.getWorkPackageLabelWidth() > 0) { g2.appendChild(SVGHelper.createText(doc, 0, 0, "WP")); g2.appendChild(SVGHelper.createText(doc, 0, 20, "Code")); g2.appendChild(SVGHelper.createText(doc, style.getWorkPackageLabelWidth(), 10, settings.getTitle())); } else { g2.appendChild(SVGHelper.createText(doc, 0, 10, settings.getTitle())); } // labelbar if (fontFamily != null) { g1 = SVGHelper.createElement(doc, "g", "transform", "translate(" + style.getTotalLabelWidth() + ",20)", "text-anchor", "middle", "font-family", fontFamily, "font-size", "9pt"); } else { g1 = SVGHelper.createElement(doc, "g", "transform", "translate(" + style.getTotalLabelWidth() + ",20)", "text-anchor", "middle", "font-size", "9pt"); } root.appendChild(g1); final Element diagram = SVGHelper.createElement(doc, "g", "transform", "translate(" + style.getTotalLabelWidth() + "," + GanttChartStyle.HEAD_HEIGHT + ")"); root.appendChild(diagram); final Element grid = SVGHelper.createElement(doc, "g", "stroke", "gray", "stroke-width", "1");// , "stroke-dasharray", "5,5"); diagram.appendChild(grid); final GanttChartXLabelBarRenderer xLabelBarRenderer = new GanttChartXLabelBarRenderer(fromDate, toDate, getDiagramWidth(), style); xLabelBarRenderer.draw(doc, g1, grid, getDiagramHeight()); // Show today line, if configured. if (style.isShowToday() == true) { final DateHolder today = new DateHolder(); if (today.isBetween(fromDate, toDate) == true) { diagram.appendChild(SVGHelper.createLine(doc, getXValue(today.getDate()), 0, getXValue(today.getDate()), getDiagramHeight(), SVGColor.RED, "stroke-width", "2")); } } // Task descriptions: if (fontFamily != null) { g1 = SVGHelper.createElement(doc, "g", "transform", "translate(5,65)", "font-family", fontFamily, "font-size", "9pt"); } else { g1 = SVGHelper.createElement(doc, "g", "transform", "translate(5,65)", "font-size", "9pt"); } root.appendChild(g1); drawGanttObjects(doc, g1, diagram, grid, allVisibleGanttObjects); g2 = SVGHelper.createElement(doc, "g", "transform", "translate(5,0)"); g1.appendChild(g2); if (fontFamily != null) { g3 = SVGHelper.createElement(doc, "g", "font-family", fontFamily, "font-size", "9pt"); } else { g3 = SVGHelper.createElement(doc, "g", "font-size", "9pt"); } g2.appendChild(g3); // diagram.appendChild(SVGHelper.createUse(doc, "#diamond", 100, 15.5 * style.getYScale())); // diagram.appendChild(SVGHelper.createRect(doc, 110, 15 * style.getYScale() + 2, 140, 16, "white")); // diagram.appendChild(SVGHelper.createText(doc, 110, 15.5 * style.getYScale() + 5, "This is a nonsens milestone.", "fill", "gray", // "font-size", "8pt")); g1 = SVGHelper.createElement(doc, "g", "transform", "translate(265,65)"); root.appendChild(g1); g1 = SVGHelper.createElement(doc, "g", "stroke", SVGColor.BLACK.getName()); root.appendChild(g1); // Show outline of canvas using 'rect' element. --> g1.appendChild(SVGHelper.createRect(doc, 0, 0, style.getWidth(), height, "none", "stroke-width", "2")); // Horizontal line after head row. g1.appendChild(SVGHelper.createLine(doc, 0, 50, style.getWidth(), 50, "stroke-width", "2")); // Vertical line between Description and bar charts. g1.appendChild(SVGHelper.createLine(doc, style.getTotalLabelWidth(), 50, style.getTotalLabelWidth(), height, "stroke-width", "2")); return doc; } /** * Recalculates all start and end dates of all nodes and the earliest calculated start date and latest calculated end date. * @return All visible nodes. */ public Collection<GanttTask> recalculate() { rootNode.recalculate(); fromDate = toDate = null; final Collection<GanttTask> allVisibleGanttObjects = getAllVisibleGanttObjects(new ArrayList<GanttTask>(), rootNode); for (final GanttTask node : allVisibleGanttObjects) { Date periodStart = GanttUtils.getCalculatedStartDate(node); Date periodEnd = GanttUtils.getCalculatedEndDate(node); if (periodEnd == null) { periodEnd = periodStart; } else if (periodStart == null) { periodStart = periodEnd; } if (fromDate == null) { fromDate = periodStart; } else if (periodStart != null && fromDate.after(periodStart) == true) { fromDate = periodStart; } if (toDate == null) { toDate = periodEnd; } else if (periodEnd != null && toDate.before(periodEnd) == true) { toDate = periodEnd; } } this.calculatedStartDate = fromDate; this.calculatedEndDate = toDate; return allVisibleGanttObjects; } private void drawGanttObjects(final Document doc, final Element g, final Element diagram, final Element grid, final Collection<GanttTask> allVisibleGanttObjects) { if (CollectionUtils.isEmpty(rootNode.getChildren()) == true) { return; } boolean first = true; for (final GanttTask node : allVisibleGanttObjects) { if (node.isVisible() == false) { continue; } final ObjectInfo taskInfo = getObjectInfo(node); drawLabel(node, doc, g); GanttObjectType type = node.getType(); if (type == null) { if (node.hasDuration() == false) { type = GanttObjectType.MILESTONE; } else { type = GanttObjectType.ACTIVITY; if (node.getChildren() != null) { for (final GanttTask child : node.getChildren()) { if (child.isVisible() == true) { type = GanttObjectType.SUMMARY; break; } } } } } else { if (type == GanttObjectType.MILESTONE == true && node.hasDuration() == true) { // Milestones can't have durations. Change it to a normal activity. type = GanttObjectType.ACTIVITY; } } if (type == GanttObjectType.MILESTONE) { // Type milestone and node has no duration. drawMilestone(node, doc, diagram); } else if (type == GanttObjectType.ACTIVITY) { drawActivity(node, doc, diagram); } else if (type == GanttObjectType.SUMMARY) { drawSummary(node, doc, diagram); } else { log.error("Unsupported type: " + node.getType()); } if (first == true) { first = false; } else { grid.appendChild(SVGHelper.createLine(doc, 0, taskInfo.y, getDiagramWidth(), taskInfo.y)); } } } private Collection<GanttTask> getAllVisibleGanttObjects(final Collection<GanttTask> col, final GanttTask node) { if (node != rootNode) { if (node.isVisible() == true) { col.add(node); } } if (CollectionUtils.isEmpty(node.getChildren()) == true) { return col; } for (final GanttTask child : node.getChildren()) { getAllVisibleGanttObjects(col, child); } return col; } private void drawLabel(final GanttTask node, final Document doc, final Element labels) { int indent = 0; GanttTask n = node; while (true) { n = rootNode.findParent(n.getId()); if (n == rootNode || n == null) { break; } if (n.isVisible() == true) { ++indent; } } final ObjectInfo taskInfo = getObjectInfo(node); if (StringUtils.isNotBlank(node.getWorkpackageCode()) == true && style.getWorkPackageLabelWidth() > 0) { labels.appendChild(SVGHelper.createText(doc, 0 + indent * 5, taskInfo.y, node.getWorkpackageCode())); } if (StringUtils.isNotBlank(node.getTitle()) == true) { labels.appendChild(SVGHelper.createText(doc, style.getWorkPackageLabelWidth() + indent * 10, taskInfo.y, node.getTitle())); } } private void drawSummary(final GanttTask node, final Document doc, final Element diagram) { final ObjectInfo taskInfo = getObjectInfo(node); if (log.isDebugEnabled() == true) { log.debug("Task added: fromDate=" + taskInfo.fromDate + " (x=" + taskInfo.x1 + "), toDate=" + taskInfo.toDate + " (x=" + taskInfo.x2); } double x1 = taskInfo.x1; double x2 = taskInfo.x2; double diagramWidth = getDiagramWidth(); if (x2 - GanttChartStyle.SUMMARY_ARROW_SIZE < 0 || x1 > diagramWidth) { return; } boolean drawLeftArrow = true; boolean drawRightArrow = true; if (x1 < 0) { x1 = 0; drawLeftArrow = false; } if (x2 > diagramWidth) { x2 = diagramWidth; drawRightArrow = false; } final double width = (x2 - x1); if (width <= 0) { return; } diagram.appendChild(SVGHelper.createRect(doc, x1, taskInfo.y + 0.2 * style.getActivityHeight(), width, 0.8 * style.getActivityHeight(), SVGColor.DARK_RED, "stroke", "none")); if (drawLeftArrow == true) { diagram.appendChild(SVGHelper.createUse(doc, "#redLeftArrow", taskInfo.x1, taskInfo.y + style.getActivityHeight())); } if (drawRightArrow == true) { diagram.appendChild(SVGHelper.createUse(doc, "#redRightArrow", (taskInfo.x2 - GanttChartStyle.SUMMARY_ARROW_SIZE), taskInfo.y + style.getActivityHeight())); } drawDependency(node, GanttObjectType.SUMMARY, doc, diagram); } private void drawActivity(final GanttTask node, final Document doc, final Element diagram) { final ObjectInfo taskInfo = getObjectInfo(node); if (taskInfo.isNaN() == true) { // No start and end date given, do nothing: return; } if (log.isDebugEnabled() == true) { log.debug("Activity added: fromDate=" + taskInfo.fromDate + " (x=" + taskInfo.x1 + "), toDate=" + taskInfo.toDate + " (x=" + taskInfo.x2 + ")"); } if (taskInfo.x2 < taskInfo.x1) { log.error("Oups, x2 < x1?: " + node); return; } double x1 = taskInfo.x1; double x2 = taskInfo.x2; double diagramWidth = getDiagramWidth(); if (x2 < 0 || x1 > diagramWidth) { return; } if (x1 < 0) { x1 = 0; } if (x2 > diagramWidth) { x2 = diagramWidth; } final double width = (x2 - x1); if (width <= 0) { return; } final double y = taskInfo.y + style.getActivityHeight() / 2; final double height = style.getActivityHeight(); if (style.isShowCompletion() == true) { Integer completion = node.getProgress(); if (completion == null || completion < 0) { completion = 0; } else if (completion > 100) { completion = 100; } final double width1 = width * completion / 100; final double width2 = width - width1; if (width1 > 0) { diagram.appendChild(SVGHelper.createRect(doc, x1, y, width1, height, SVGColor.DARK_BLUE, SVGColor.DARK_BLUE)); } if (width2 > 0) { diagram.appendChild(SVGHelper.createRect(doc, x1 + width1, y, width2, height, SVGColor.LIGHT_BLUE, SVGColor.DARK_BLUE)); } } else { diagram.appendChild(SVGHelper.createRect(doc, x1, y, width, height, SVGColor.DARK_BLUE, SVGColor.NONE)); } drawDependency(node, GanttObjectType.ACTIVITY, doc, diagram); } private void drawMilestone(final GanttTask node, final Document doc, final Element diagram) { final ObjectInfo taskInfo = getObjectInfo(node); final Date date = taskInfo.fromDate != null ? taskInfo.fromDate : taskInfo.toDate; if (date == null) { // Neither start nor end date given, do nothing: return; } final double x = getXValue(date); if (x < 0 || x > getDiagramWidth()) { return; } if (log.isDebugEnabled() == true) { log.debug("Milestone added: date=" + date + " (x=" + x + ")"); } diagram.appendChild(SVGHelper.createUse(doc, "#diamond", x, taskInfo.y + style.getYScale() / 2)); drawDependency(node, GanttObjectType.MILESTONE, doc, diagram); } private void drawDependency(final GanttTask node, final GanttObjectType objectType, final Document doc, final Element diagram) { final ObjectInfo taskInfo = getObjectInfo(node); if (node.getPredecessor() != null) { final double dist; if (objectType == GanttObjectType.MILESTONE) { dist = 5; } else { dist = 1; } final ObjectInfo depObjectInfo = getObjectInfo(node.getPredecessor()); if (depObjectInfo.isVisible() == true) { final GanttRelationType type = node.getRelationType() != null ? node.getRelationType() : GanttRelationType.FINISH_START; final double depX1; final double depX2; final double depY1 = depObjectInfo.y + style.getActivityHeight(); final double depY2 = taskInfo.y + style.getActivityHeight(); if (type == GanttRelationType.START_START) { depX1 = depObjectInfo.x1; depX2 = taskInfo.x1; } else if (type == GanttRelationType.START_FINISH) { depX1 = depObjectInfo.x1; depX2 = taskInfo.x2; } else if (type == GanttRelationType.FINISH_START) { depX1 = depObjectInfo.x2; depX2 = taskInfo.x1; } else { depX1 = depObjectInfo.x2; depX2 = taskInfo.x2; } double diagramWidth = getDiagramWidth(); if (depX1 > 0 && depX1 < diagramWidth && depX2 > 0 && depX2 < diagramWidth) { diagram.appendChild(SVGHelper.createPath(doc, SVGColor.NONE, 1, SVGColor.BLACK, SVGHelper.drawHorizontalConnectionLine(type, depX1, depY1, depX2, depY2, style.getArrowMinXDist() + dist))); if (type.isIn(GanttRelationType.FINISH_START, GanttRelationType.START_START) == true) { diagram.appendChild(SVGHelper.createPath(doc, SVGColor.BLACK, 1, SVGColor.BLACK, SVGHelper.drawArrow(ArrowDirection.RIGHT, depX2 - dist, depY2, style.getArrowSize()))); } else { diagram.appendChild(SVGHelper.createPath(doc, SVGColor.BLACK, 1, SVGColor.BLACK, SVGHelper.drawArrow(ArrowDirection.LEFT, depX2 + dist / 2, depY2, style.getArrowSize()))); } } } else if (log.isDebugEnabled() == true) { log.debug("Depend on task is invisible, so cannot draw dependency."); } } } private double getDiagramWidth() { return style.getWidth() - style.getTotalLabelWidth(); } private double getDiagramHeight() { return height - GanttChartStyle.HEAD_HEIGHT; } private double getXValue(final Date date) { if (date == null) { return 0.0; } final DateHolder dh = new DateHolder(fromDate); final int days = dh.daysBetween(date); final int fromToDays = getFromToDays(); if (fromToDays == 0) { return 0; } final int hourOfDay = new DateHolder(date).getHourOfDay(); return this.getDiagramWidth() * (days * 24 + hourOfDay) / (fromToDays * 24); } private int getFromToDays() { if (fromToDays < 0) { final DateHolder dh = new DateHolder(fromDate); fromToDays = dh.daysBetween(toDate); } return fromToDays; } }