/* GanttProject is an opensource project management tool. Copyright (C) 2004-2011 Dmitry Barashev, GanttProject Team This program 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. 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 General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package net.sourceforge.ganttproject.chart; import biz.ganttproject.core.calendar.CalendarEvent; import biz.ganttproject.core.chart.canvas.Canvas; import biz.ganttproject.core.chart.canvas.Painter; import biz.ganttproject.core.chart.grid.*; import biz.ganttproject.core.chart.grid.OffsetManager.OffsetBuilderFactory; import biz.ganttproject.core.chart.render.TextLengthCalculatorImpl; import biz.ganttproject.core.chart.scene.DayGridSceneBuilder; import biz.ganttproject.core.chart.scene.SceneBuilder; import biz.ganttproject.core.chart.scene.TimelineSceneBuilder; import biz.ganttproject.core.chart.text.TimeFormatter; import biz.ganttproject.core.chart.text.TimeFormatters; import biz.ganttproject.core.chart.text.TimeUnitText.Position; import biz.ganttproject.core.option.*; import biz.ganttproject.core.time.TimeDuration; import biz.ganttproject.core.time.TimeUnit; import biz.ganttproject.core.time.TimeUnitFunctionOfDate; import biz.ganttproject.core.time.TimeUnitStack; import com.google.common.base.Function; import com.google.common.base.Predicate; import com.google.common.collect.Lists; import com.google.common.collect.Sets; import net.sourceforge.ganttproject.chart.item.CalendarChartItem; import net.sourceforge.ganttproject.chart.item.ChartItem; import net.sourceforge.ganttproject.chart.item.TimelineLabelChartItem; import net.sourceforge.ganttproject.gui.UIConfiguration; import net.sourceforge.ganttproject.gui.UIFacade; import net.sourceforge.ganttproject.language.GanttLanguage; import net.sourceforge.ganttproject.language.GanttLanguage.Event; import net.sourceforge.ganttproject.task.Task; import net.sourceforge.ganttproject.task.TaskManager; import java.awt.*; import java.awt.image.BufferedImage; import java.text.DateFormat; import java.util.*; import java.util.List; /** * Controls painting of the common part of Gantt and resource charts (in * particular, timeline). Calculates data required by the specific charts (e.g. * calculates the offsets of the timeline grid cells) */ public abstract class ChartModelBase implements /* TimeUnitStack.Listener, */ChartModel, TimelineLabelRendererImpl.ChartModelApi { public static interface ScrollingSession { void scrollTo(int xpos, int ypos); void finish(); } private class ScrollingSessionImpl implements ScrollingSession { private int myPrevXpos; private OffsetList myTopOffsets; private OffsetList myBottomOffsets; private OffsetList myDefaultOffsets; private ScrollingSessionImpl(int startXpos) { // System.err.println("start xpos=" + startXpos); myPrevXpos = startXpos; ChartModelBase.this.myScrollingSession = this; ChartModelBase.this.myOffsetManager.reset(); myTopOffsets = getTopUnitOffsets(); myBottomOffsets = getBottomUnitOffsets(); myDefaultOffsets = getDefaultUnitOffsets(); // shiftOffsets(-myBottomOffsets.get(0).getOffsetPixels()); // System.err.println(myBottomOffsets.subList(0, 3)); } @Override public void scrollTo(int xpos, int ypos) { int shift = xpos - myPrevXpos; // System.err.println("xpos="+xpos+" shift=" + shift); shiftOffsets(shift); if (myBottomOffsets.get(0).getOffsetPixels() > 0) { int currentExceed = myBottomOffsets.get(0).getOffsetPixels(); ChartModelBase.this.setStartDate(getBottomUnit().jumpLeft(getStartDate())); ChartModelBase.this.myOffsetManager.constructOffsets(); shiftOffsets(-myBottomOffsets.get(1).getOffsetPixels() + currentExceed); // System.err.println("one time unit to the left. start date=" + // ChartModelBase.this.getStartDate()); // System.err.println(myBottomOffsets.subList(0, 3)); } else if (myBottomOffsets.get(1).getOffsetPixels() <= 0) { ChartModelBase.this.setStartDate(myBottomOffsets.get(2).getOffsetStart()); ChartModelBase.this.myOffsetManager.constructOffsets(); shiftOffsets(-myBottomOffsets.get(0).getOffsetPixels()); // System.err.println("one time unit to the right. start date=" + // ChartModelBase.this.getStartDate()); // System.err.println(myBottomOffsets.subList(0, 3)); } myPrevXpos = xpos; } @Override public void finish() { Offset offset0 = myBottomOffsets.get(0); Offset offset1 = myBottomOffsets.get(1); int middle = (offset1.getOffsetPixels() + offset0.getOffsetPixels()) / 2; if (middle < 0) { ChartModelBase.this.setStartDate(myBottomOffsets.get(2).getOffsetStart()); } ChartModelBase.this.myScrollingSession = null; } private void shiftOffsets(int shiftPixels) { myBottomOffsets.shift(shiftPixels); myTopOffsets.shift(shiftPixels); if (myDefaultOffsets != myBottomOffsets) { if (myDefaultOffsets.isEmpty()) { myDefaultOffsets = ChartModelBase.this.getDefaultUnitOffsets(); } myDefaultOffsets.shift(shiftPixels); } } } public static final Object STATIC_MUTEX = new Object(); private static final Predicate<? super Task> MILESTONE_PREDICATE = new Predicate<Task>() { @Override public boolean apply(Task input) { return input.isMilestone(); } }; private final OptionEventDispatcher myOptionEventDispatcher = new OptionEventDispatcher(); private Dimension myBounds; private Date myStartDate; protected int myAtomUnitPixels; protected final TimeUnitStack myTimeUnitStack; private TimeUnit myTopUnit; protected TimeUnit myBottomUnit; private final TimelineSceneBuilder myChartHeader; private final BackgroundRendererImpl myBackgroundRenderer; private final StyledPainterImpl myPainter; private final List<GPOptionChangeListener> myOptionListeners = new ArrayList<GPOptionChangeListener>(); private final UIConfiguration myProjectConfig; private ChartUIConfiguration myChartUIConfiguration; private final List<SceneBuilder> myRenderers = Lists.newArrayList(); private final DayGridSceneBuilder myChartGrid; private final TimelineLabelRendererImpl myTimelineLabelRenderer; protected final TaskManager myTaskManager; private int myVerticalOffset; private int myHorizontalOffset; private ScrollingSessionImpl myScrollingSession; private Set<Task> myTimelineTasks = Collections.emptySet(); private final ChartOptionGroup myChartGridOptions; private final GPOptionGroup myTimelineLabelOptions; private final BooleanOption myTimelineMilestonesOption = new DefaultBooleanOption("timeline.showMilestones", true); private final FontOption myChartFontOption; public ChartModelBase(TaskManager taskManager, TimeUnitStack timeUnitStack, final UIConfiguration projectConfig) { myTaskManager = taskManager; myProjectConfig = projectConfig; myChartUIConfiguration = new ChartUIConfiguration(projectConfig); myChartFontOption = projectConfig.getChartFontOption(); myPainter = new StyledPainterImpl(myChartUIConfiguration); myTimeUnitStack = timeUnitStack; final TimeFormatters.LocaleApi localeApi = new TimeFormatters.LocaleApi() { @Override public Locale getLocale() { return GanttLanguage.getInstance().getDateFormatLocale(); } @Override public DateFormat createDateFormat(String pattern) { return GanttLanguage.getInstance().createDateFormat(pattern); } @Override public DateFormat getShortDateFormat() { return GanttLanguage.getInstance().getShortDateFormat(); } @Override public String i18n(String key) { return GanttLanguage.getInstance().getText(key); } }; final TimeFormatters timeFormatters = new TimeFormatters(localeApi); GanttLanguage.getInstance().addListener(new GanttLanguage.Listener() { @Override public void languageChanged(Event event) { timeFormatters.setLocaleApi(localeApi); } }); myChartHeader = new TimelineSceneBuilder(new TimelineSceneBuilder.InputApi() { @Override public Date getViewportStartDate() { return getStartDate(); } @Override public OffsetList getTopUnitOffsets() { return ChartModelBase.this.getTopUnitOffsets(); } @Override public int getTopLineHeight() { return getChartUIConfiguration().getSpanningHeaderHeight(); } @Override public int getTimelineHeight() { return getChartUIConfiguration().getHeaderHeight(); } @Override public Color getTimelineBorderColor() { return getChartUIConfiguration().getHeaderBorderColor(); } @Override public Color getTimelineBackgroundColor() { return getChartUIConfiguration().getSpanningHeaderBackgroundColor(); } @Override public int getViewportWidth() { return getBounds().width; } @Override public OffsetList getBottomUnitOffsets() { return ChartModelBase.this.getBottomUnitOffsets(); } @Override public TimeFormatter getFormatter(TimeUnit timeUnit, Position position) { return timeFormatters.getFormatter(timeUnit, position); } }); myChartGridOptions = new ChartOptionGroup("ganttChartGridDetails", new GPOption[] { projectConfig.getRedlineOption(), projectConfig.getProjectBoundariesOption(), projectConfig.getWeekendAlphaRenderingOption(), myChartUIConfiguration.getChartStylesOption()}, getOptionEventDispatcher()); myChartGrid = new DayGridSceneBuilder(new DayGridSceneBuilder.InputApi() { @Override public Color getWeekendColor() { return myChartUIConfiguration.getHolidayTimeBackgroundColor(); } @Override public Color getHolidayColor(Date holiday) { CalendarEvent event = getTaskManager().getCalendar().getEvent(holiday); if (event == null || event.getColor() == null) { return null; } return event.getColor(); } public CalendarEvent getEvent(Date date) { return getTaskManager().getCalendar().getEvent(date); } @Override public int getTopLineHeight() { return myChartUIConfiguration.getSpanningHeaderHeight(); } @Override public BooleanOption getRedlineOption() { return projectConfig.getRedlineOption(); } @Override public Date getProjectStart() { return getTaskManager().getProjectStart(); } @Override public Date getProjectEnd() { return getTaskManager().getProjectEnd(); } @Override public BooleanOption getProjectDatesOption() { return projectConfig.getProjectBoundariesOption(); } @Override public OffsetList getAtomUnitOffsets() { return getDefaultUnitOffsets(); } }, myChartHeader.getTimelineContainer()); myBackgroundRenderer = new BackgroundRendererImpl(this); myTimelineLabelOptions = new ChartOptionGroup("timelineLabels", new GPOption[] { myTimelineMilestonesOption }, getOptionEventDispatcher()); myTimelineLabelRenderer = new TimelineLabelRendererImpl(this); addRenderer(myBackgroundRenderer); addRenderer(myChartHeader); addRenderer(myChartGrid); addRenderer(myTimelineLabelRenderer); ChangeValueListener fontChangeValueListener = new ChangeValueListener() { @Override public void changeValue(ChangeValueEvent event) { setBaseFont(myChartFontOption.getValue()); } }; myChartFontOption.addChangeValueListener(fontChangeValueListener); getProjectConfig().getDpiOption().addChangeValueListener(fontChangeValueListener); setBaseFont(myChartFontOption.getValue()); } private void setBaseFont(FontSpec fontSpec) { float scaleFactor = fontSpec.getSize().getFactor(); if (getProjectConfig().getDpiOption() != null) { scaleFactor *= getProjectConfig().getDpiOption().getValue().floatValue() / UIFacade.DEFAULT_DPI; } Font font = new Font(fontSpec.getFamily(), Font.PLAIN, (int)(10*scaleFactor)); BufferedImage dummyImage = new BufferedImage(100, 100, BufferedImage.TYPE_INT_BGR); Graphics2D g = (Graphics2D) dummyImage.getGraphics(); TextLengthCalculatorImpl calculator = new TextLengthCalculatorImpl(g); int fontSize = calculator.getTextHeight(font, "Agpqf"); getChartUIConfiguration().setBaseFont(font, fontSize); } private OffsetManager myOffsetManager = new OffsetManager(new OffsetBuilderFactory() { @Override public OffsetBuilder createTopAndBottomUnitBuilder() { return createOffsetBuilderFactory().build(); } @Override public OffsetBuilder createAtomUnitBuilder() { int defaultUnitCountPerLastBottomUnit = OffsetBuilderImpl.getConcreteUnit( getBottomUnit(), getEndDate()).getAtomCount(getDefaultUnit()); return createOffsetBuilderFactory() .withRightMargin(myScrollingSession == null ? 0 : defaultUnitCountPerLastBottomUnit * 2) .withTopUnit(getBottomUnit()) .withBottomUnit(getTimeUnitStack().getDefaultTimeUnit()) .withOffsetStepFunction(new Function<TimeUnit, Float>() { @Override public Float apply(TimeUnit timeUnit) { int offsetUnitCount = timeUnit.getAtomCount(getTimeUnitStack().getDefaultTimeUnit()); return 1f / offsetUnitCount; } }).build(); } }); @Override public void resetOffsets() { myOffsetManager.reset(); } @Override public OffsetList getTopUnitOffsets() { return myOffsetManager.getTopUnitOffsets(); } @Override public OffsetList getBottomUnitOffsets() { return myOffsetManager.getBottomUnitOffsets(); } @Override public OffsetList getDefaultUnitOffsets() { if (getBottomUnit().equals(getTimeUnitStack().getDefaultTimeUnit())) { return getBottomUnitOffsets(); } return myOffsetManager.getAtomUnitOffsets(); } Date getOffsetAnchorDate() { return /* * myScrollingSession == null ? myStartDate : */getBottomUnit().jumpLeft(myStartDate); } public OffsetBuilder.Factory createOffsetBuilderFactory() { OffsetBuilder.Factory factory = new OffsetBuilderImpl.FactoryImpl() .withAtomicUnitWidth(getBottomUnitWidth()) .withBottomUnit(getBottomUnit()) .withCalendar(myTaskManager.getCalendar()) .withRightMargin(myScrollingSession == null ? 0 : 1) .withStartDate(getOffsetAnchorDate()) .withViewportStartDate(getStartDate()) .withTopUnit(myTopUnit) .withWeekendDecreaseFactor( getTopUnit().isConstructedFrom(getBottomUnit()) ? OffsetBuilderImpl.WEEKEND_UNIT_WIDTH_DECREASE_FACTOR : 1f); if (getBounds() != null) { factory.withEndOffset((int) getBounds().getWidth()); } return factory; } @Override public void paint(Graphics g) { int height = (int) getBounds().getHeight(); for (SceneBuilder renderer : getRenderers()) { renderer.reset(height); } for (SceneBuilder renderer : getRenderers()) { renderer.build(); } myPainter.setGraphics(g); for (SceneBuilder renderer : getRenderers()) { renderer.getCanvas().paint(myPainter); } for (int layer = 0;; layer++) { boolean layerPainted = false; for (SceneBuilder renderer : getRenderers()) { List<Canvas> layers = renderer.getCanvas().getLayers(); if (layer < layers.size()) { layers.get(layer).paint(myPainter); layerPainted = true; } } if (!layerPainted) { break; } } } protected List<SceneBuilder> getRenderers() { return myRenderers; } @Override public void addRenderer(SceneBuilder renderer) { myRenderers.add(renderer); } protected Painter getPainter() { return myPainter; } public void resetRenderers() { myRenderers.clear(); } @Override public void setBounds(Dimension bounds) { if (bounds != null && bounds.equals(myBounds)) { return; } myBounds = bounds; myOffsetManager.reset(); } @Override public void setStartDate(Date startDate) { myHorizontalOffset = 0; if (!startDate.equals(myStartDate)) { myStartDate = startDate; myOffsetManager.reset(); } } @Override public Date getStartDate() { return myStartDate; } @Override public Date getEndDate() { List<Offset> offsets = getBottomUnitOffsets(); return offsets.isEmpty() ? null : offsets.get(offsets.size() - 1).getOffsetEnd(); } @Override public void setBottomUnitWidth(int pixelsWidth) { if (pixelsWidth == myAtomUnitPixels) { return; } myAtomUnitPixels = pixelsWidth; myOffsetManager.reset(); } @Override public void setRowHeight(int rowHeight) { getChartUIConfiguration().setRowHeight(rowHeight); } @Override public void setTopTimeUnit(TimeUnit topTimeUnit) { setTopUnit(topTimeUnit); } @Override public void setBottomTimeUnit(TimeUnit bottomTimeUnit) { if (bottomTimeUnit.equals(myBottomUnit)) { return; } myBottomUnit = bottomTimeUnit; myOffsetManager.reset(); } protected UIConfiguration getProjectConfig() { return myProjectConfig; } @Override public Dimension getBounds() { return myBounds; } // @Override // public Dimension getMaxBounds() { // OffsetBuilderImpl offsetBuilder = new OffsetBuilderImpl( // this, Integer.MAX_VALUE, getTaskManager().getProjectEnd()); // List<Offset> topUnitOffsets = new ArrayList<Offset>(); // OffsetList bottomUnitOffsets = new OffsetList(); // offsetBuilder.constructOffsets(topUnitOffsets, bottomUnitOffsets); // int width = topUnitOffsets.get(topUnitOffsets.size()-1).getOffsetPixels(); // int height = calculateRowHeight()*getRowCount(); // return new Dimension(width, height); // } public abstract int calculateRowHeight(); // protected abstract int getRowCount(); @Override public int getBottomUnitWidth() { return myAtomUnitPixels; } @Override public TimeUnitStack getTimeUnitStack() { return myTimeUnitStack; } @Override public ChartUIConfiguration getChartUIConfiguration() { return myChartUIConfiguration; } @Override public int getTimelineTopLineHeight() { return getChartUIConfiguration().getSpanningHeaderHeight(); } private void setChartUIConfiguration(ChartUIConfiguration chartConfig) { myChartUIConfiguration = chartConfig; } @Override public TaskManager getTaskManager() { return myTaskManager; } @Override public Offset getOffsetAt(int x) { for (Offset offset : getDefaultUnitOffsets()) { if (offset.getOffsetPixels() >= x) { // System.err.println("result=" + offset); return offset; } } List<Offset> offsets = getBottomUnitOffsets(); return offsets.get(offsets.size() - 1); } /** * @return A length of the visible part of this chart area measured in the * bottom line time units */ public TimeDuration getVisibleLength() { double pixelsLength = getBounds().getWidth(); float unitsLength = (float) (pixelsLength / getBottomUnitWidth()); TimeDuration result = getTaskManager().createLength(getBottomUnit(), unitsLength); return result; } public void setHeaderHeight(int i) { getChartUIConfiguration().setHeaderHeight(i); } @Override public void setVerticalOffset(int offset) { myVerticalOffset = offset; } protected int getVerticalOffset() { return myVerticalOffset; } public void setHorizontalOffset(int pixels) { myHorizontalOffset = pixels; } protected int getHorizontalOffset() { return myHorizontalOffset; } @Override public TimeUnit getBottomUnit() { return myBottomUnit; } private TimeUnit getDefaultUnit() { return getTimeUnitStack().getDefaultTimeUnit(); } private void setTopUnit(TimeUnit topUnit) { if (topUnit.equals(myTopUnit)) { return; } this.myTopUnit = topUnit; myOffsetManager.reset(); } public TimeUnit getTopUnit() { return getTopUnit(myStartDate); } private TimeUnit getTopUnit(Date startDate) { TimeUnit result = myTopUnit; if (myTopUnit instanceof TimeUnitFunctionOfDate) { if (startDate == null) { throw new RuntimeException("No date is set"); } result = ((TimeUnitFunctionOfDate) myTopUnit).createTimeUnit(startDate); } return result; } public GPOptionGroup[] getChartOptionGroups() { return new GPOptionGroup[] { myChartGridOptions, myTimelineLabelOptions }; } public void addOptionChangeListener(GPOptionChangeListener listener) { myOptionListeners.add(listener); } protected void fireOptionsChanged() { for (GPOptionChangeListener next : myOptionListeners) { next.optionsChanged(); } } public abstract ChartModelBase createCopy(); protected void setupCopy(ChartModelBase copy) { copy.setTopTimeUnit(getTopUnit()); copy.setBottomTimeUnit(getBottomUnit()); copy.setBottomUnitWidth(getBottomUnitWidth()); copy.setStartDate(getStartDate()); copy.setChartUIConfiguration(myChartUIConfiguration.createCopy()); copy.setBounds(getBounds()); copy.setTimelineTasks(myTimelineTasks); copy.myTimelineMilestonesOption.setValue(myTimelineMilestonesOption.getValue()); GPOptionGroup[] copyOptions = copy.getChartOptionGroups(); GPOptionGroup[] thisOptions = getChartOptionGroups(); assert copyOptions.length == thisOptions.length; for (int i = 0; i < copyOptions.length; i++) { copyOptions[i].copyFrom(thisOptions[i]); } copy.myChartFontOption.setValue(myChartFontOption.getValue()); copy.calculateRowHeight(); } @Override public OptionEventDispatcher getOptionEventDispatcher() { return myOptionEventDispatcher; } public class OptionEventDispatcher { void optionsChanged() { fireOptionsChanged(); } } public ScrollingSession createScrollingSession(int startXpos) { assert myScrollingSession == null; return new ScrollingSessionImpl(startXpos); } public ChartItem getChartItemWithCoordinates(int x, int y) { Canvas.Shape text = myTimelineLabelRenderer.getLabelLayer().getPrimitive(x, y); if (text instanceof Canvas.Text) { return new TimelineLabelChartItem((Task)text.getModelObject()); } Offset offset = getOffsetAt(x); if (offset != null) { return new CalendarChartItem(offset.getOffsetStart()); } return null; } @Override public Collection<Task> getTimelineTasks() { return Sets.union(myTimelineTasks, getMilestones()); } private Set<Task> getMilestones() { return myTimelineMilestonesOption.getValue() ? Sets.filter(Sets.newHashSet(getTaskManager().getTasks()), MILESTONE_PREDICATE) : Collections.<Task>emptySet(); } public void setTimelineTasks(Set<Task> timelineTasks) { myTimelineTasks = timelineTasks; } }