/*
Copyright 2004-2013 BarD Software s.r.o
This file is part of GanttProject, an opensource project management tool.
GanttProject 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, either version 3 of the License, or
(at your option) any later version.
GanttProject 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 GanttProject. If not, see <http://www.gnu.org/licenses/>.
*/
package biz.ganttproject.core.calendar;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Collection;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import biz.ganttproject.core.calendar.CalendarEvent.Type;
import biz.ganttproject.core.calendar.walker.ForwardTimeWalker;
import biz.ganttproject.core.time.CalendarFactory;
import biz.ganttproject.core.time.TimeDuration;
import biz.ganttproject.core.time.TimeUnit;
import biz.ganttproject.core.time.impl.FramerImpl;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
/**
* Implements a calendar which is aware of weekend days and recurring/one-off holidays and working days.
*
* By default, this calendar assumes that any day D is a working day. Then it applies weekend check,
* recurring event check and one-off event check in this specified order. Each check can override the result of the previous
* one. For instance, if D is a weekend day then it is a holiday, unless one of the following is the case:
* -- only show weekends option is on
* -- there is one-off event at date D with type WORKING
* -- there is a recurring event at date D with type WORKING and no one-off event at date D with type HOLIDAY
*
* If D is a non-weekend day then it is a working day, unless one of the following is the case:
* -- there is one-off event at date D with type HOLIDAY
* -- there is a recurring event at date D with type HOLIDAY and no one-off event at date D with type WORKING
*
* @author dbarashev (Dmitry Barashev)
*/
public class WeekendCalendarImpl extends GPCalendarBase implements GPCalendarCalc {
private final Calendar myCalendar = CalendarFactory.newCalendar();
private final FramerImpl myFramer = new FramerImpl(Calendar.DAY_OF_WEEK);
private final DayType[] myTypes = new DayType[7];
private boolean myOnlyShowWeekends = false;
private int myWeekendDaysCount;
private final Map<Date, CalendarEvent> myRecurringEvents = Maps.newLinkedHashMap();
private final Map<Date, CalendarEvent> myOneOffEvents = Maps.newLinkedHashMap();
private final AlwaysWorkingTimeCalendarImpl myRestlessCalendar = new AlwaysWorkingTimeCalendarImpl();
private String myBaseCalendarID;
public WeekendCalendarImpl() {
this(null);
}
public WeekendCalendarImpl(String baseCalendarID) {
myBaseCalendarID = baseCalendarID;
reset();
}
public void reset() {
myRecurringEvents.clear();
myOneOffEvents.clear();
for (int i = 0; i < myTypes.length; i++) {
myTypes[i] = GPCalendar.DayType.WORKING;
}
setWeekDayType(GregorianCalendar.SATURDAY, GPCalendar.DayType.WEEKEND);
setWeekDayType(GregorianCalendar.SUNDAY, GPCalendar.DayType.WEEKEND);
fireCalendarChanged();
}
@Override
public List<GPCalendarActivity> getActivities(Date startDate, final Date endDate) {
if (getWeekendDaysCount() == 0 && myOneOffEvents.isEmpty() && myRecurringEvents.isEmpty()) {
return myRestlessCalendar.getActivities(startDate, endDate);
}
List<GPCalendarActivity> result = new ArrayList<GPCalendarActivity>();
Date curDayStart = myFramer.adjustLeft(startDate);
boolean isWeekendState = (getDayMask(curDayStart) & DayMask.WORKING) == 0;
while (curDayStart.before(endDate)) {
Date changeStateDayStart = doFindClosest(curDayStart, myFramer, MoveDirection.FORWARD,
isWeekendState ? DayType.WORKING : DayType.NON_WORKING, endDate);
if (changeStateDayStart == null) {
changeStateDayStart = endDate;
}
if (changeStateDayStart.before(endDate) == false) {
result.add(new CalendarActivityImpl(curDayStart, endDate, !isWeekendState));
break;
}
result.add(new CalendarActivityImpl(curDayStart, changeStateDayStart, !isWeekendState));
curDayStart = changeStateDayStart;
isWeekendState = !isWeekendState;
}
return result;
}
public boolean isWeekend(Date curDayStart) {
if (myOnlyShowWeekends) {
return false;
}
myCalendar.setTime(curDayStart);
int dayOfWeek = myCalendar.get(Calendar.DAY_OF_WEEK);
return myTypes[dayOfWeek - 1] == GPCalendar.DayType.WEEKEND;
}
@Override
protected List<GPCalendarActivity> getActivitiesForward(Date startDate, TimeUnit timeUnit, final long unitCount) {
final List<GPCalendarActivity> result = new ArrayList<GPCalendarActivity>();
new ForwardTimeWalker(this, timeUnit) {
long myUnitCount = unitCount;
@Override
protected void processWorkingTime(Date intervalStart, Date nextIntervalStart) {
result.add(new CalendarActivityImpl(intervalStart, nextIntervalStart, true));
myUnitCount--;
}
@Override
protected void processNonWorkingTime(Date intervalStart, Date workingIntervalStart) {
result.add(new CalendarActivityImpl(intervalStart, workingIntervalStart, false));
}
@Override
protected boolean isMoving() {
return myUnitCount > 0;
}
}.walk(startDate);
return result;
}
@Override
protected List<GPCalendarActivity> getActivitiesBackward(Date startDate, TimeUnit timeUnit, long unitCount) {
List<GPCalendarActivity> result = new LinkedList<GPCalendarActivity>();
Date unitStart = timeUnit.adjustLeft(startDate);
while (unitCount > 0) {
Date prevUnitStart = timeUnit.jumpLeft(unitStart);
boolean isWeekendState = (getDayMask(prevUnitStart) & DayMask.WORKING) == 0;
if (isWeekendState) {
Date lastWorkingUnitStart = findClosest(prevUnitStart, timeUnit, MoveDirection.BACKWARD, DayType.WORKING);
Date firstWeekendUnitStart = timeUnit.adjustRight(lastWorkingUnitStart);
Date lastWeekendUnitEnd = unitStart;
result.add(0, new CalendarActivityImpl(firstWeekendUnitStart, lastWeekendUnitEnd, false));
unitStart = firstWeekendUnitStart;
} else {
result.add(0, new CalendarActivityImpl(prevUnitStart, unitStart, true));
unitCount--;
unitStart = prevUnitStart;
}
}
return result;
}
@Override
public void setWeekDayType(int day, DayType type) {
if (type != myTypes[day - 1]) {
myWeekendDaysCount += (type == DayType.WEEKEND ? 1 : -1);
}
myTypes[day - 1] = type;
fireCalendarChanged();
}
@Override
public DayType getWeekDayType(int day) {
return myTypes[day - 1];
}
@Override
public boolean getOnlyShowWeekends() {
return myOnlyShowWeekends;
}
@Override
public void setOnlyShowWeekends(boolean onlyShowWeekends) {
myOnlyShowWeekends = onlyShowWeekends;
fireCalendarChanged();
}
private int getWeekendDaysCount() {
return myOnlyShowWeekends ? 0 : myWeekendDaysCount;
}
@Override
public Date findClosestWorkingTime(Date time) {
if (getWeekendDaysCount() == 0 && myRecurringEvents.isEmpty() && myOneOffEvents.isEmpty()) {
return time;
}
int dayMask = getDayMask(time);
if ((dayMask & DayMask.WORKING) == DayMask.WORKING) {
return time;
}
return doFindClosest(time, myFramer, MoveDirection.FORWARD, DayType.WORKING, null);
}
private boolean isPublicHoliDay(Date curDayStart) {
CalendarEvent oneOff = myOneOffEvents.get(curDayStart);
if (oneOff != null) {
switch (oneOff.getType()) {
case HOLIDAY:
return true;
case WORKING_DAY:
return false;
case NEUTRAL:
default:
// intentionally fall-through, consult recurring holidays in this case
}
}
CalendarEvent recurring = myRecurringEvents.get(getRecurringDate(curDayStart));
if (recurring != null) {
switch (recurring.getType()) {
case HOLIDAY:
return true;
case WORKING_DAY:
return false;
case NEUTRAL:
default:
// intentionally fall-through, use default answer
}
}
return false;
}
public CalendarEvent getEvent(Date date) {
CalendarEvent result = myOneOffEvents.get(date);
if (result == null) {
result = myRecurringEvents.get(getRecurringDate(date));
}
return result;
}
private Date getRecurringDate(Date date) {
myCalendar.setTime(date);
myCalendar.set(Calendar.YEAR, 1);
return myCalendar.getTime();
}
@Override
public int getDayMask(Date date) {
int result = 0;
myCalendar.setTime(date);
int dayOfWeek = myCalendar.get(Calendar.DAY_OF_WEEK);
boolean isHoliday = isPublicHoliDay(date);
boolean isWeekend = myTypes[dayOfWeek - 1] == DayType.WEEKEND;
if (isWeekend) {
result |= DayMask.WEEKEND;
CalendarEvent oneOff = myOneOffEvents.get(date);
if (oneOff != null && oneOff.getType() == Type.WORKING_DAY) {
result |= DayMask.WORKING;
}
}
if (isHoliday) {
result |= DayMask.HOLIDAY;
return result;
}
if (!isWeekend || myOnlyShowWeekends) {
result |= DayMask.WORKING;
}
return result;
}
// @Override
// public boolean isNonWorkingDay(Date curDayStart) {
// return isWeekend(curDayStart) || isPublicHoliDay(curDayStart);
// }
@Override
public void setPublicHolidays(Collection<CalendarEvent> holidays) {
myRecurringEvents.clear();
myOneOffEvents.clear();
for (CalendarEvent h : holidays) {
if (h.isRecurring) {
myCalendar.setTime(h.myDate);
myCalendar.set(Calendar.YEAR, 1);
myRecurringEvents.put(myCalendar.getTime(), h);
} else {
myOneOffEvents.put(h.myDate, h);
}
}
fireCalendarChanged();
// myCalendarUrl = calendarUrl;
// clearPublicHolidays();
// if (calendarUrl != null) {
// XMLCalendarOpen opener = new XMLCalendarOpen();
//
// HolidayTagHandler tagHandler = new HolidayTagHandler(this);
//
// opener.addTagHandler(tagHandler);
// opener.addParsingListener(tagHandler);
// try {
// opener.load(calendarUrl.openStream());
// } catch (Exception e) {
// throw new RuntimeException(e);
// }
// }
}
@Override
public Collection<CalendarEvent> getPublicHolidays() {
List<CalendarEvent> result = Lists.newArrayListWithExpectedSize(myRecurringEvents.size() + myOneOffEvents.size());
result.addAll(myRecurringEvents.values());
result.addAll(myOneOffEvents.values());
return result;
}
@Override
public List<GPCalendarActivity> getActivities(Date startingFrom, TimeDuration period) {
return getActivities(startingFrom, period.getTimeUnit(), period.getLength());
}
@Override
public GPCalendarCalc copy() {
WeekendCalendarImpl result = new WeekendCalendarImpl(myBaseCalendarID);
for (int i = 1; i < 8; i++) {
result.setWeekDayType(i, getWeekDayType(i));
}
result.setOnlyShowWeekends(getOnlyShowWeekends());
result.setPublicHolidays(getPublicHolidays());
//result.publicHolidaysArray.addAll(publicHolidaysArray);
return result;
}
@Override
public String getBaseCalendarID() {
return myBaseCalendarID;
}
@Override
public void setBaseCalendarID(String id) {
myBaseCalendarID = id;
}
@Override
public void importCalendar(GPCalendar calendar, ImportCalendarOption importOption) {
if (ImportCalendarOption.Values.NO.equals(importOption.getSelectedValue())) {
return;
}
if (ImportCalendarOption.Values.REPLACE.equals(importOption.getSelectedValue())) {
reset();
setPublicHolidays(calendar.getPublicHolidays());
for (int i = 1; i <= 7; i++) {
setWeekDayType(i, calendar.getWeekDayType(i));
}
return;
}
if (ImportCalendarOption.Values.MERGE.equals(importOption.getSelectedValue())) {
LinkedHashSet<CalendarEvent> mergedHolidays = Sets.newLinkedHashSet(getPublicHolidays());
mergedHolidays.addAll(calendar.getPublicHolidays());
setPublicHolidays(mergedHolidays);
}
for (int i = 1; i <= 7; i++) {
if (calendar.getWeekDayType(i) == DayType.WEEKEND) {
setWeekDayType(i, DayType.WEEKEND);
}
}
}
}