/////////////////////////////////////////////////////////////////////////////
//
// 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.Calendar;
import java.util.Date;
import org.projectforge.calendar.DayHolder;
import org.projectforge.common.DateHolder;
import org.projectforge.export.SVGColor;
import org.projectforge.export.SVGHelper;
import org.projectforge.user.PFUserContext;
import org.projectforge.web.calendar.DateTimeFormatter;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
public class GanttChartXLabelBarRenderer
{
private static final org.apache.log4j.Logger log = org.apache.log4j.Logger.getLogger(GanttChartXLabelBarRenderer.class);
private static final double MIN_PIXEL_PER_UNIT = 10.0;
private static final int[] QUARTER_SCALES = { 1, 2, 4, 8, 12};
private static final int[] MONTH_SCALES = { 1, 2, 3, 6, 12, 24, 36, 48, 60};
private static final int[] SCALES = { 1, 2, 5, 10, 20, 50, 100, 200, 500, 1000};
GanttXUnit labelUnit;
int labelScale;
// Ticks with labels
GanttXUnit ticksUnit;
int ticksScale;
// Small ticks
GanttXUnit ticksUnit2;
int ticksScale2;
GanttXUnit gridUnit;
int gridScale;
String label;
private boolean showNonWorkingDays;
private Date fromDate;
private Date toDate;
int fromToDays = -1;
private double diagramWidth;
private GanttChartStyle style;
public GanttChartXLabelBarRenderer(final Date fromDate, final Date toDate, final double diagramWidth, final GanttChartStyle style)
{
this.fromDate = fromDate;
this.toDate = toDate;
this.diagramWidth = diagramWidth;
this.style = style;
this.gridScale = style.getXGridScale();
init();
}
private void init()
{
final int numberOfDays = getFromToDays(); // Initialize fromToDays.
final double numberOfWeeks = 0.25 * numberOfDays;
final double numberOfMonths = 0.033 * numberOfDays;
final double numberOfQuarters = numberOfMonths / 3;
if (this.style.getXUnit() == GanttXUnit.AUTO) {
if (diagramWidth / numberOfDays > MIN_PIXEL_PER_UNIT) {
labelUnit = GanttXUnit.DAY;
ticksUnit = GanttXUnit.DAY;
ticksUnit2 = GanttXUnit.DAY;
} else if (diagramWidth / numberOfWeeks > MIN_PIXEL_PER_UNIT) {
labelUnit = GanttXUnit.WEEK;
ticksUnit = GanttXUnit.WEEK;
ticksUnit2 = GanttXUnit.DAY;
} else if (diagramWidth / numberOfMonths > MIN_PIXEL_PER_UNIT) {
labelUnit = GanttXUnit.MONTH;
ticksUnit = GanttXUnit.MONTH;
ticksUnit2 = GanttXUnit.WEEK;
} else {
labelUnit = GanttXUnit.QUARTER;
ticksUnit = GanttXUnit.MONTH;
ticksUnit2 = GanttXUnit.WEEK;
}
}
this.gridUnit = ticksUnit;
double numberOfUnits;
if (labelUnit == GanttXUnit.DAY) {
numberOfUnits = (double) numberOfDays;
} else if (labelUnit == GanttXUnit.WEEK) {
numberOfUnits = numberOfWeeks;
} else if (labelUnit == GanttXUnit.MONTH) {
numberOfUnits = numberOfMonths;
} else {
numberOfUnits = numberOfQuarters;
}
labelScale = style.getXLabelsScale();
ticksScale = style.getXTicksScale();
ticksScale2 = style.getXTicksScale2();
if (labelUnit == GanttXUnit.QUARTER) {
if (labelScale < 0) {
if (style.isRelativeTimeValues() == true) {
// Numbers doesn't need as much space as quarter labels (03/2010)
labelScale = getScale(numberOfQuarters, 50, labelUnit);
} else {
labelScale = getScale(numberOfQuarters, 100, labelUnit);
}
}
if (ticksScale < 0) {
ticksScale = 4;
if (labelScale <= ticksScale) {
ticksScale = labelScale;
}
}
if (ticksScale2 < 0) {
ticksScale = 1;
}
} else {
if (labelScale < 0) {
if (style.isRelativeTimeValues() == true) {
// Numbers doesn't need as much space as date labels (03/2010)
labelScale = getScale(numberOfUnits, 50, labelUnit);
} else {
labelScale = getScale(numberOfUnits, 100, labelUnit);
}
}
if (ticksScale < 0) {
ticksScale = 5;
if (labelScale <= ticksScale) {
ticksScale = labelScale;
}
}
if (ticksScale2 < 0) {
ticksScale = 1;
}
}
if (style.getXLabel() != null) {
label = style.getXLabel();
} else if (style.isRelativeTimeValues() == true) {
label = PFUserContext.getLocalizedString(labelUnit.getI18nKey());
}
this.showNonWorkingDays = style.isShowNonWorkingDays();
if (this.showNonWorkingDays == true) {
if (diagramWidth / fromToDays < 2) {
// Don't show non working days due to the large scale. At minimum 3 pixels per day required.
this.showNonWorkingDays = false;
}
}
}
public int getScale(final double numberOfUnits, final int minPixels, final GanttXUnit unit)
{
final int[] scales;
if (unit == GanttXUnit.QUARTER) {
scales = QUARTER_SCALES;
} else if (unit == GanttXUnit.MONTH) {
scales = MONTH_SCALES;
} else {
scales = SCALES;
}
double widthPerUnit = diagramWidth / numberOfUnits;
int current = 1;
for (int scale : scales) {
current = scale;
if (widthPerUnit * scale > minPixels) {
break;
}
}
return current;
}
/**
* @param doc
* @param g1
* @param grid If null then no grid will be drawn.
* @param xGridHeight
*/
public void draw(final Document doc, final Element g1, final Element grid, final double xGridHeight)
{
if (label != null) {
g1.appendChild(SVGHelper.createText(doc, diagramWidth / 2, 0, label));
}
final Element ticks = SVGHelper.createElement(doc, "g", "stroke", SVGColor.BLACK.getName(), "stroke-width", "1", "transform", "translate(0,10)");
g1.appendChild(ticks);
final DayHolder day = new DayHolder(fromDate);
final DayHolder toDay = new DayHolder(toDate);
int dayCounter = 0;
int weekCounter = 0;
int monthCounter = 0;
int quarterCounter = 0;
int lastDateLabel = 0;
boolean nonWorkingDayDisplayed = false;
while (day.before(toDay) == true) {
if (dayCounter > 0) {
if (showNonWorkingDays == true && xGridHeight > 0) {
if (day.isWorkingDay() == true) {
nonWorkingDayDisplayed = false;
} else if (nonWorkingDayDisplayed == false) {
// Non-working day:
showNonWorkingDays(doc, grid, day.getDate(), toDay.getDate(), xGridHeight);
nonWorkingDayDisplayed = true;
}
}
int wc = -1;
if (day.getDayOfWeek() == day.getCalendar().getFirstDayOfWeek()) {
wc = ++weekCounter;
}
int mc = -1;
int qc = -1;
final int dayOfMonth = day.getDayOfMonth();
final int month = day.getMonth();
if (dayOfMonth == 1) {
mc = ++monthCounter;
if (month == Calendar.JANUARY || month == Calendar.APRIL || month == Calendar.JULY || month == Calendar.OCTOBER) {
qc = ++quarterCounter;
}
}
boolean drawLabel = match(labelScale, labelUnit, dayCounter, wc, mc, qc);
boolean drawTick = match(ticksScale, ticksUnit, dayCounter, wc, mc, qc);
boolean drawTick2 = match(ticksScale2, ticksUnit2, dayCounter, wc, mc, qc);
boolean drawGrid = match(gridScale, gridUnit, dayCounter, wc, mc, qc);
if (style.isRelativeTimeValues() == false) {
if (labelUnit == GanttXUnit.DAY) {
if (labelScale <= 5) {
// So draw labels on 1st, 5th, 10th, 15th, 20th, 25th of month.
drawLabel = (dayOfMonth == 1 || dayOfMonth == 5 || dayOfMonth == 10 || dayOfMonth == 15 || dayOfMonth == 20 || dayOfMonth == 25);
} else if (labelScale <= 15) {
// So draw labels on 1st, 15th.
drawLabel = (dayOfMonth == 1 || dayOfMonth == 15);
} else if (labelScale <= 15) {
// So draw labels on 1st.
drawLabel = (dayOfMonth == 1);
} else {
if (dayOfMonth == 1 && dayCounter > lastDateLabel + labelScale) {
drawLabel = true;
lastDateLabel = dayCounter;
} else {
drawLabel = false;
}
}
} else if (labelUnit == GanttXUnit.WEEK) {
if (dayOfMonth == 1 && dayCounter > lastDateLabel + labelScale) {
drawLabel = true;
lastDateLabel = dayCounter;
} else {
drawLabel = false;
}
}
}
if (drawLabel == true) {
String label = null;
if (style.isRelativeTimeValues() == false) {
label = DateTimeFormatter.instance().getFormattedDate(day.getDate());
} else if (labelUnit == GanttXUnit.DAY) {
label = String.valueOf(dayCounter);
} else if (labelUnit == GanttXUnit.WEEK) {
label = String.valueOf(weekCounter);
} else if (labelUnit == GanttXUnit.MONTH) {
label = String.valueOf(monthCounter);
} else {
label = String.valueOf(quarterCounter);
}
g1.appendChild(SVGHelper.createText(doc, getXValue(day.getDate()), 22, label, "text-anchor", "middle"));
}
if (drawLabel == true) {
ticks.appendChild(SVGHelper.createLine(doc, getXValue(day.getDate()), 12, getXValue(day.getDate()), 20));
} else if (drawTick == true) {
ticks.appendChild(SVGHelper.createLine(doc, getXValue(day.getDate()), 15, getXValue(day.getDate()), 20));
} else if (drawTick2 == true) {
ticks.appendChild(SVGHelper.createLine(doc, getXValue(day.getDate()), 18, getXValue(day.getDate()), 20));
}
if (grid != null && (drawGrid == true || drawLabel == true)) {
grid.appendChild(SVGHelper.createLine(doc, getXValue(day.getDate()), 0, getXValue(day.getDate()), xGridHeight));
}
}
day.add(Calendar.DAY_OF_MONTH, 1);
if (dayCounter++ > 5000) {
log.error("Endless loop detection while creating x tick labels. Breaking.");
break;
}
}
ticks.appendChild(SVGHelper.createLine(doc, 0, 0, diagramWidth, 0));
ticks.appendChild(SVGHelper.createLine(doc, 0, 0, 0, 20));
}
/**
* @param doc
* @param g
* @param day
* @param toDate Last day of diagram.
* @param lastNonWorkingDay
* @param height
*/
private void showNonWorkingDays(final Document doc, final Element g, final Date day, final Date toDate, final double height)
{
if (g == null) {
return;
}
final DayHolder dh = new DayHolder(day);
final double x1 = getXValue(day);
for (int i = 0; i < 100; i++) { // End-less loop protection.
dh.add(Calendar.DAY_OF_MONTH, 1);
if (dh.isWorkingDay() == true || dh.before(toDate) == false) {
break;
}
}
final double x2 = getXValue(dh.getDate());
g.appendChild(SVGHelper.createRect(doc, x1, 0, x2 - x1, height, SVGColor.LIGHT_GRAY, SVGColor.NONE));
}
private boolean match(final int scale, final GanttXUnit unit, final int dayCounter, final int weekCounter, final int monthCounter,
final int quarterCounter)
{
if (unit == GanttXUnit.DAY) {
if (dayCounter % scale == 0) {
return true;
}
} else if (unit == GanttXUnit.WEEK) {
if (weekCounter > 0 && weekCounter % scale == 0) {
return true;
}
} else if (unit == GanttXUnit.MONTH) {
if (monthCounter > 0 && monthCounter % scale == 0) {
return true;
}
} else if (unit == GanttXUnit.QUARTER) {
if (quarterCounter > 0 && quarterCounter % scale == 0) {
return true;
}
}
return false;
}
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;
}
return diagramWidth * days / fromToDays;
}
private int getFromToDays()
{
if (fromToDays < 0) {
final DateHolder dh = new DateHolder(fromDate);
fromToDays = dh.daysBetween(toDate);
}
return fromToDays;
}
}