/*
* This file is part of LibrePlan
*
* Copyright (C) 2009-2010 Fundación para o Fomento da Calidade Industrial e
* Desenvolvemento Tecnolóxico de Galicia
* Copyright (C) 2010-2011 Igalia, S.L.
*
* 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 Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.zkoss.ganttz.timetracker;
import static org.zkoss.ganttz.i18n.I18nHelper._;
import java.util.Collection;
import java.util.Date;
import org.apache.commons.lang3.Validate;
import org.joda.time.LocalDate;
import org.zkoss.ganttz.DatesMapperOnInterval;
import org.zkoss.ganttz.IDatesMapper;
import org.zkoss.ganttz.data.Task;
import org.zkoss.ganttz.timetracker.zoom.DetailItem;
import org.zkoss.ganttz.timetracker.zoom.IDetailItemModifier;
import org.zkoss.ganttz.timetracker.zoom.IZoomLevelChangedListener;
import org.zkoss.ganttz.timetracker.zoom.SeveralModifiers;
import org.zkoss.ganttz.timetracker.zoom.TimeTrackerState;
import org.zkoss.ganttz.timetracker.zoom.ZoomLevel;
import org.zkoss.ganttz.util.Interval;
import org.zkoss.ganttz.util.LongOperationFeedback;
import org.zkoss.ganttz.util.WeakReferencedListeners;
import org.zkoss.ganttz.util.LongOperationFeedback.ILongOperation;
import org.zkoss.zk.ui.Component;
public class TimeTracker {
public interface IDetailItemFilter {
Collection<DetailItem> selectsFirstLevel(Collection<DetailItem> firstLevelDetails);
Collection<DetailItem> selectsSecondLevel(Collection<DetailItem> secondLevelDetails);
Interval getCurrentPaginationInterval();
void resetInterval();
}
private ZoomLevel detailLevel = ZoomLevel.DETAIL_ONE;
private WeakReferencedListeners<IZoomLevelChangedListener> zoomListeners = WeakReferencedListeners.create();
private IDatesMapper datesMapper = null;
private Collection<DetailItem> detailsFirstLevelCached = null;
private Collection<DetailItem> detailsSecondLevelCached = null;
private Interval interval;
private final IDetailItemModifier firstLevelModifier;
private final IDetailItemModifier secondLevelModifier;
private final Component componentOnWhichGiveFeedback;
private boolean registeredFirstTask = false;
private IDetailItemFilter filter = null;
private Interval realIntervalCached;
public TimeTracker(Interval interval, ZoomLevel zoomLevel, Component parent) {
this(interval, zoomLevel, SeveralModifiers.empty(), SeveralModifiers.empty(), parent);
}
public TimeTracker(Interval interval, Component componentOnWhichGiveFeedback) {
this(interval, SeveralModifiers.empty(), SeveralModifiers.empty(), componentOnWhichGiveFeedback);
}
public TimeTracker(
Interval interval,
IDetailItemModifier firstLevelModifier,
IDetailItemModifier secondLevelModifier,
Component componentOnWhichGiveFeedback) {
Validate.notNull(interval);
Validate.notNull(firstLevelModifier);
Validate.notNull(secondLevelModifier);
Validate.notNull(componentOnWhichGiveFeedback);
this.interval = interval;
this.firstLevelModifier = firstLevelModifier;
this.secondLevelModifier = secondLevelModifier;
this.componentOnWhichGiveFeedback = componentOnWhichGiveFeedback;
}
public TimeTracker(
Interval interval,
ZoomLevel zoomLevel,
IDetailItemModifier firstLevelModifier,
IDetailItemModifier secondLevelModifier,
Component componentOnWhichGiveFeedback) {
this(interval, firstLevelModifier, secondLevelModifier, componentOnWhichGiveFeedback);
detailLevel = zoomLevel;
}
public IDetailItemFilter getFilter() {
return filter;
}
public void setFilter(IDetailItemFilter filter) {
this.filter = filter;
datesMapper = null;
}
public ZoomLevel getDetailLevel() {
return detailLevel;
}
public void addZoomListener(IZoomLevelChangedListener listener) {
zoomListeners.addListener(listener);
}
public Collection<DetailItem> getDetailsFirstLevel() {
if ( detailsFirstLevelCached == null ) {
detailsFirstLevelCached = getTimeTrackerState().getFirstLevelDetails(interval);
}
return filterFirstLevel(detailsFirstLevelCached);
}
private Collection<DetailItem> filterFirstLevel(Collection<DetailItem> firstLevelDetails) {
return filter == null ? firstLevelDetails : filter.selectsFirstLevel(firstLevelDetails);
}
public Collection<DetailItem> getDetailsSecondLevel() {
if ( detailsSecondLevelCached == null ) {
detailsSecondLevelCached = getTimeTrackerState().getSecondLevelDetails(interval);
}
return filterSecondLevel(detailsSecondLevelCached);
}
private Collection<DetailItem> filterSecondLevel(Collection<DetailItem> secondLevelDetails) {
return filter == null ? secondLevelDetails : filter.selectsSecondLevel(secondLevelDetails);
}
public Interval getRealInterval() {
if ( realIntervalCached == null ) {
realIntervalCached = getTimeTrackerState().getRealIntervalFor(interval);
}
return realIntervalCached;
}
public TimeTrackerState getTimeTrackerState() {
return detailLevel.getTimeTrackerState(firstLevelModifier, secondLevelModifier);
}
private void fireZoomChanged() {
/* Do not replace it with lambda */
zoomListeners.fireEvent(new WeakReferencedListeners.IListenerNotification<IZoomLevelChangedListener>() {
@Override
public void doNotify(IZoomLevelChangedListener listener) {
listener.zoomLevelChanged(detailLevel);
}
});
}
public int getHorizontalSize() {
int horizontalSize = 0;
for (DetailItem detailItem : getDetailsSecondLevel()) {
horizontalSize += detailItem.getSize();
}
return horizontalSize;
}
private void clearDetailLevelDependantData() {
datesMapper = null;
detailsFirstLevelCached = detailsSecondLevelCached = null;
realIntervalCached = null;
}
public void resetMapper() {
datesMapper = null;
realIntervalCached = null;
}
public IDatesMapper getMapper() {
if ( datesMapper == null ) {
if ( filter == null ) {
datesMapper = new DatesMapperOnInterval(getHorizontalSize(), getRealInterval());
} else {
datesMapper = new DatesMapperOnInterval(getHorizontalSize(), filter.getCurrentPaginationInterval());
}
}
return datesMapper;
}
public void zoomIncrease() {
detailLevel = detailLevel.next();
invalidatingChangeHappenedWithFeedback();
}
private void invalidatingChangeHappenedWithFeedback() {
LongOperationFeedback.execute(componentOnWhichGiveFeedback, new ILongOperation() {
@Override
public void doAction() {
invalidatingChangeHappened();
}
@Override
public String getName() {
return _("changing zoom");
}
});
}
public void setZoomLevel(ZoomLevel zoomLevel) {
detailLevel = zoomLevel;
invalidatingChangeHappenedWithFeedback();
}
private void invalidatingChangeHappened() {
clearDetailLevelDependantData();
fireZoomChanged();
}
public void zoomDecrease() {
detailLevel = detailLevel.previous();
invalidatingChangeHappenedWithFeedback();
}
public void trackPosition(final Task task) {
task.addFundamentalPropertiesChangeListener(evt -> updateIntervalIfNeeded(task));
updateIntervalIfNeeded(task);
}
private void updateIntervalIfNeeded(Task task) {
if ( !registeredFirstTask ) {
registeredFirstTask = true;
interval = new Interval(startMinusTwoWeeks(task), endPlusOneMonth(task));
invalidatingChangeHappened();
} else {
LocalDate newStart = interval.getStart();
LocalDate newFinish = interval.getFinish();
boolean changed = false;
if ( interval.getStart().compareTo(startMinusTwoWeeks(task) ) > 0) {
newStart = startMinusTwoWeeks(task);
changed = true;
}
if ( interval.getFinish().compareTo(endPlusOneMonth(task)) < 0 ) {
newFinish = endPlusOneMonth(task);
changed = true;
}
if ( changed ) {
interval = new Interval(newStart, newFinish);
invalidatingChangeHappened();
}
}
}
private Date max(Date date1, Date date2) {
if ( date1 == null ) {
return date2;
}
if ( date2 == null ) {
return date1;
}
return date1.compareTo(date2) > 0 ? date1 : date2;
}
private Date min(Date date1, Date date2) {
if ( date1 == null ) {
return date2;
}
if ( date2 == null ) {
return date1;
}
return date1.compareTo(date2) <= 0 ? date1 : date2;
}
private LocalDate endPlusOneMonth(Task task) {
return new LocalDate(max(task.getEndDate().toDayRoundedDate(), task.getDeadline())).plusMonths(1);
}
private LocalDate startMinusTwoWeeks(Task task) {
// The deadline could be before the start
Date start = min(task.getBeginDate().toDayRoundedDate(), task.getDeadline());
// The last consolidated value could be before the start
if ( task.getConsolidatedline() != null ) {
start = min(start, task.getConsolidatedline().toDayRoundedDate());
}
return new LocalDate(start).minusWeeks(2);
}
}