Java tutorial
/* * Copyright 2014 Tomi Virtanen * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.tltv.gantt.client; import java.util.Date; import java.util.HashSet; import java.util.LinkedHashMap; import java.util.Map; import java.util.Map.Entry; import java.util.Set; import org.tltv.gantt.client.shared.Resolution; import com.google.gwt.core.client.GWT; import com.google.gwt.dom.client.DivElement; import com.google.gwt.dom.client.Element; import com.google.gwt.dom.client.Style.Display; import com.google.gwt.dom.client.Style.Unit; import com.google.gwt.i18n.client.TimeZone; import com.google.gwt.i18n.shared.DateTimeFormat; import com.google.gwt.user.client.DOM; import com.google.gwt.user.client.ui.AbstractNativeScrollbar; import com.google.gwt.user.client.ui.Widget; /** * GWT widget to build a scalable timeline that supports more than one * resolutions ({@link org.tltv.gantt.client.shared.Resolution}). When timeline * element doesn't overflow horizontally in it's parent element, it scales the * content width up to fit in the space available. * <p> * When this component scales up, all widths are calculated as percentages. * Pixel widths are used otherwise. Some browsers may not support percentages * accurately enough, and for those it's best to call * {@link #setAlwaysCalculatePixelWidths(boolean)} with 'true' to disable * percentage values. * <p> * There's always a minimum width calculated and updated to the timeline * element. Percentage values set some limitation for the component's width. * Wider the component (> 4000px), bigger the change to get year, month and date * blocks not being vertically in-line with each others. * <p> * Supports setting a scroll left position. * <p> * After construction, attach the component to it's parent and call update * method with a required parameters and the timeline is ready. After that, all * widths are calculated and all other API methods available can be used safely. * * @author Tltv * */ public class TimelineWidget extends Widget { public static final String STYLE_TIMELINE = "timeline"; public static final String STYLE_ROW = "row"; public static final String STYLE_COL = "col"; public static final String STYLE_MONTH = "month"; public static final String STYLE_YEAR = "year"; public static final String STYLE_DAY = "day"; public static final String STYLE_RESOLUTION = "resolution"; public static final String STYLE_WEEK_FIRST = "week-f"; public static final String STYLE_WEEK_LAST = "week-l"; public static final String STYLE_WEEK_MIDDLE = "week-m"; public static final String STYLE_EVEN = "even"; public static final String STYLE_WEEKEND = "weekend"; public static final String STYLE_SPACER = "spacer"; public static final int DAYS_IN_WEEK = 7; public static final int HOURS_IN_DAY = 24; public static final long DAY_INTERVAL = 24 * 60 * 60 * 1000; public static final long HOUR_INTERVAL = 60 * 60 * 1000; public static final int RESOLUTION_WEEK_DAYBLOCK_WIDTH = 4; public static final int RESOLUTION_HOUR_DAYBLOCK_WIDTH = 4; private boolean ie, ie8, ie9; private boolean forceUpdateFlag; private TimeZone gmt = TimeZone.createTimeZone(0); private LocaleDataProvider localeDataProvider; private DateTimeFormat yearDateTimeFormat; private DateTimeFormat monthDateTimeFormat; private DateTimeFormat weekDateTimeFormat; private DateTimeFormat dayDateTimeFormat; private DateTimeFormat hour12DateTimeFormat; private DateTimeFormat hour24DateTimeFormat; private boolean even; private String locale; private Resolution resolution; private long startDate; private long endDate; private int firstDayOfWeek; private int lastDayOfWeek; private int firstDayOfRange; private int firstHourOfRange; private String[] monthNames; private String[] weekdayNames; /* single resolution block interval in milliseconds */ private long interval; /* * number of blocks in resolution range. Days for Day/Week resolution, Hours * for hour resolution.. */ private int blocksInRange = 0; private int firstResBlockCount; private int lastResBlockCount; private boolean firstWeek; private boolean firstDay; private boolean timelineOverflowingHorizontally; private boolean noticeVerticalScrollbarWidth; private boolean monthRowVisible; private boolean yearRowVisible; private String monthFormat; private String yearFormat; private String weekFormat; private String dayFormat; private DivElement resolutionDiv; private DivElement resSpacerDiv; private Set<DivElement> spacerBlocks = new HashSet<DivElement>(); private BlockRowData yearRowData = new BlockRowData(); private BlockRowData monthRowData = new BlockRowData(); // days/daysLength are needed only with resolutions smaller than Day. private BlockRowData dayRowData = new BlockRowData(); private int minResolutionWidth = -1; private int minWidth = -1; private boolean calcPixels = false; enum Weekday { First, Between, Last } /** * Constructs the widget. Call * {@link #update(Resolution, long, long, int, LocaleDataProvider)} after * the component is attached to some parent widget. */ public TimelineWidget() { setElement(DivElement.as(DOM.createDiv())); setStyleName(STYLE_TIMELINE); } /** * <p> * Updates the content of this widget. Builds the time-line and calculates * width and heights for the content (calls in the end * {@link #updateWidths()}). This should be called explicitly. Otherwise the * widget will be empty. * <p> * Date values should always follow specification in {@link Date#getTime()}. * Start and end date is always required. * * @param resolution * Resolution enum (not null) * @param startDate * Time-line's start date in milliseconds. (not null) * @param endDate * Time-line's end date in milliseconds. (not null) * @param firstDayOfRange * First day of the whole range. Allowed values are 1-7. 1 is * Sunday. Required with {@link Resolution#Week}. * @param firstHourOfRange * First hour of the range. Allowed values are 0-23. Required * with {@link Resolution#Hour}. * @param localeDataProvider * Data provider for locale specific data. month names, first day * of week etc. * */ public void update(Resolution resolution, long startDate, long endDate, int firstDayOfRange, int firstHourOfRange, LocaleDataProvider localeDataProvider) { if (localeDataProvider == null) { GWT.log(getClass().getSimpleName() + " requires LocaleDataProvider. Can't complete update(...) operation."); return; } if (isChanged(resolution, startDate, endDate, localeDataProvider.getFirstDayOfWeek(), firstDayOfRange, firstHourOfRange, localeDataProvider.getLocale())) { clear(); GWT.log(getClass().getSimpleName() + " content cleared."); } else { return; } GWT.log(getClass().getSimpleName() + " Updating content."); locale = localeDataProvider.getLocale(); this.resolution = resolution; this.startDate = startDate; this.endDate = endDate; // Required with Resolution.Week. firstDayOfWeek = localeDataProvider.getFirstDayOfWeek(); lastDayOfWeek = (firstDayOfWeek == 1) ? 7 : Math.max((firstDayOfWeek - 1) % 8, 1); this.firstDayOfRange = firstDayOfRange; this.firstHourOfRange = firstHourOfRange; monthNames = localeDataProvider.getMonthNames(); weekdayNames = localeDataProvider.getWeekdayNames(); this.localeDataProvider = localeDataProvider; resolutionDiv = DivElement.as(DOM.createDiv()); resolutionDiv.setClassName(STYLE_ROW + " " + STYLE_RESOLUTION); if (minResolutionWidth < 0) { minResolutionWidth = calculateResolutionMinWidth(); } if (resolution == Resolution.Day || resolution == Resolution.Week) { prepareTimelineForDayResolution(startDate, endDate); } else if (resolution == Resolution.Hour) { prepareTimelineForHourResolution(startDate, endDate); } else { GWT.log(getClass().getSimpleName() + " resolution " + (resolution != null ? resolution.name() : "null") + " is not supported"); return; } if (isYearRowVisible()) { appendTimelineBlocks(yearRowData, STYLE_YEAR); } if (isMonthRowVisible()) { appendTimelineBlocks(monthRowData, STYLE_MONTH); } if (isDayRowVisible()) { appendTimelineBlocks(dayRowData, STYLE_DAY); } getElement().appendChild(resolutionDiv); GWT.log(getClass().getSimpleName() + " Constructed content."); updateWidths(); GWT.log(getClass().getSimpleName() + " is updated for resolution " + resolution.name() + "."); } /** * Set minimum width (pixels) of this widget's root DIV element. Default is * -1. Notice that {@link #update(Resolution, long, long)} will calculate * min-width and call this internally. * * @param minWidth * Minimum width in pixels. */ public void setMinWidth(int minWidth) { this.minWidth = minWidth; getElement().getStyle().setProperty("minWidth", this.minWidth + "px"); } /** * Return minimum width (pixels) of this widget's root DIV element. Returns * -1 if not set. * * @return min-width */ public int getMinWidth() { return minWidth; } /** * Calculate matching left offset in percentage for a date ( * {@link Date#getTime()}). * * @param date * Target date in milliseconds. * @return Left offset in percentage. */ public double getLeftPositionPercentageForDate(long date) { double left = getLeftPositionForDate(date); double width = getResolutionWidth(); return (100.0 / width) * left; } /** * Calculate CSS value for 'left' property matching left offset in * percentage for a date ( {@link Date#getTime()}). * <p> * May return '2.123456%' or 'calc(2.123456%)' if IE(>8); * * @param date * Target date in milliseconds. * @return Left offset as a String value. */ public String getLeftPositionPercentageStringForDate(long date) { double left = getLeftPositionForDate(date); double width = getResolutionWidth(); String calc = createCalcCssValue(width, left); if (calc != null) { return calc; } return (100.0 / width) * left + "" + Unit.PCT.getType(); } /** * Calculate CSS value for 'width' property matching date interval inside * the time-line. Returns percentage value. Interval is in milliseconds. * <p> * May return '2.123456%' or 'calc(2.123456%)' if IE(>8); * * @param interval * Date interval in milliseconds. * @return */ public String getWidthPercentageStringForDateInterval(long interval) { double range = endDate - startDate; String calc = createCalcCssValue(range, interval); if (calc != null) { return calc; } return (100.0 / range) * interval + "" + Unit.PCT.getType(); } /** * Calculate matching left offset in pixels for a date ( * {@link Date#getTime()}). * * @param date * Target date in milliseconds. * @return Left offset in pixels. */ public double getLeftPositionForDate(long date) { double width = getResolutionWidth(); double range = endDate - startDate; if (range <= 0) { return 0; } double p = width / range; double offset = date - startDate; double left = p * offset; return left; } /** * Calculate matching date ({@link Date#getTime()}) for the target left * pixel offset. * * @param left * Left offset in pixels. * @return Date in a milliseconds. */ public long getDateForLeftPosition(double left) { double width = getResolutionWidth(); double range = endDate - startDate; if (width <= 0) { return 0; } double p = range / width; double offset = p * left; double date = startDate + offset; return (long) date; } /** * Set horizontal scroll position for the time-line. * * @param left * Scroll position in pixels. */ public void setScrollLeft(double left) { getElement().getStyle().setLeft(-left, Unit.PX); } /** * Re-calculates required widths for this widget. */ public void updateWidths() { if (resolutionDiv == null) { GWT.log(getClass().getSimpleName() + " is not ready for updateWidths() call. Call update(...) instead."); return; } GWT.log(getClass().getSimpleName() + " Started updating widths."); setMinWidth(blocksInRange * minResolutionWidth); // update horizontal overflow state here, after min-width is updated. updateTimelineOverflowingHorizontally(); // remove spacer block if it exist removeResolutionSpacerBlock(); // now when the spacer block is removed, count resolution block elements int resolutionBlockCount = resolutionDiv.getChildCount(); // calculate new block width for day-resolution. // Year and month blocks are vertically in-line with days. double dayWidthPercentage = 100.0 / blocksInRange; double dayWidthPx = Math.round(resolutionDiv.getClientWidth() / blocksInRange); while ((resolutionDiv.getClientWidth() % (blocksInRange * dayWidthPx)) >= blocksInRange) { dayWidthPx++; } // calculate block width for currently selected resolution // (day,week,...) // resolution div's content may not be vertically in-line with // year/month blocks. This is the case for example with Week resolution. double resBlockMinWidthPx = minResolutionWidth; double resBlockWidthPx = dayWidthPx; double resBlockWidthPercentage = 100.0 / resolutionBlockCount; String pct = createCalcCssValue(resolutionBlockCount); if (resolution == Resolution.Week) { resBlockMinWidthPx = DAYS_IN_WEEK * minResolutionWidth; resBlockWidthPx = DAYS_IN_WEEK * dayWidthPx; resBlockWidthPercentage = dayWidthPercentage * DAYS_IN_WEEK; pct = createCalcCssValue(blocksInRange, DAYS_IN_WEEK); } // update resolution block widths updateResolutionBlockWidths(resolutionBlockCount, dayWidthPercentage, dayWidthPx, resBlockMinWidthPx, resBlockWidthPx, resBlockWidthPercentage, pct); if (isYearRowVisible()) { // update year block widths updateBlockWidths(dayWidthPercentage, dayWidthPx, yearRowData); } if (isMonthRowVisible()) { // update month block widths updateBlockWidths(dayWidthPercentage, dayWidthPx, monthRowData); } if (isDayRowVisible()) { updateBlockWidths(dayWidthPercentage, dayWidthPx, dayRowData); } if (isAlwaysCalculatePixelWidths()) { updateSpacerBlocks(dayWidthPx); } GWT.log(getClass().getSimpleName() + " Widths are updated."); } /** * Returns true if the timeline is overflowing the parent's width. This * works only when this widget is attached to some parent. * * @return True when timeline width is more than the parent's width (@see * {@link Element#getClientWidth()}). */ public boolean isTimelineOverflowingHorizontally() { return timelineOverflowingHorizontally; } /** * Updates horizontal overflow state and returns true if the timeline is * overflowing the parent's width. This works only when this widget is * attached to some parent. * * @return True when timeline width is more than the parent's width (@see * {@link Element#getClientWidth()}). */ public boolean checkTimelineOverflowingHorizontally() { updateTimelineOverflowingHorizontally(); return isTimelineOverflowingHorizontally(); } /** * Return true if timeline should notice vertical scrollbar width in it's * calculations. * * @return */ public boolean isNoticeVerticalScrollbarWidth() { return noticeVerticalScrollbarWidth; } public void setNoticeVerticalScrollbarWidth(boolean noticeVerticalScrollbarWidth) { this.noticeVerticalScrollbarWidth = noticeVerticalScrollbarWidth; if (noticeVerticalScrollbarWidth) { getElement().getStyle().setMarginRight(AbstractNativeScrollbar.getNativeScrollbarWidth(), Unit.PX); } else { getElement().getStyle().clearMarginRight(); } } public void setBrowserInfo(boolean ie, boolean ie8, boolean ie9) { this.ie = ie; this.ie8 = ie8; this.ie9 = ie9; } /** * Tells this Widget to calculate widths by itself. Percentage widths are * not used. Some browsers may not handle sub-pixel calculating accurately * enough. Setting this to true works as a fallback mode for those browsers. * <p> * Default value is false. * * @param calcPx * @return */ public void setAlwaysCalculatePixelWidths(boolean calcPx) { calcPixels = calcPx; } /** * Returns true if Widget is set to calculate widths by itself. Default is * false. * * @return */ public boolean isAlwaysCalculatePixelWidths() { return calcPixels; } /** * Get actual width of the timeline. * * @return */ public double getResolutionWidth() { double width = resolutionDiv.getClientWidth(); if (isAlwaysCalculatePixelWidths() && resSpacerDiv != null && resSpacerDiv.hasParentElement()) { width = width - resSpacerDiv.getClientWidth(); } return width; } public boolean isDayRowVisible() { return resolution == Resolution.Hour; } public boolean isMonthRowVisible() { return monthRowVisible; } public boolean isYearRowVisible() { return yearRowVisible; } public void setMonthRowVisible(boolean monthRowVisible) { this.monthRowVisible = monthRowVisible; } public void setYearRowVisible(boolean yearRowVisible) { this.yearRowVisible = yearRowVisible; } public String getMonthFormat() { return monthFormat; } public void setMonthFormat(String monthFormat) { this.monthFormat = monthFormat; } public String getYearFormat() { return yearFormat; } public void setYearFormat(String yearFormat) { this.yearFormat = yearFormat; } public void setWeekFormat(String weekFormat) { this.weekFormat = weekFormat; } public void setDayFormat(String dayFormat) { this.dayFormat = dayFormat; } /** * Sets force update flag up. Next * {@link #update(Resolution, long, long, int, LocaleDataProvider)} call * knows then to update everything. */ public void setForceUpdate() { forceUpdateFlag = true; } public DateTimeFormat getYearDateTimeFormat() { if (yearDateTimeFormat == null) { yearDateTimeFormat = DateTimeFormat.getFormat("yyyy"); } return yearDateTimeFormat; } public DateTimeFormat getMonthDateTimeFormat() { if (monthDateTimeFormat == null) { monthDateTimeFormat = DateTimeFormat.getFormat("M"); } return monthDateTimeFormat; } public DateTimeFormat getWeekDateTimeFormat() { if (weekDateTimeFormat == null) { weekDateTimeFormat = DateTimeFormat.getFormat("d"); } return weekDateTimeFormat; } public DateTimeFormat getDayDateTimeFormat() { if (dayDateTimeFormat == null) { dayDateTimeFormat = DateTimeFormat.getFormat("d"); } return dayDateTimeFormat; } public DateTimeFormat getHour12DateTimeFormat() { if (hour12DateTimeFormat == null) { hour12DateTimeFormat = DateTimeFormat.getFormat("h"); } return hour12DateTimeFormat; } public DateTimeFormat getHour24DateTimeFormat() { if (hour24DateTimeFormat == null) { hour24DateTimeFormat = DateTimeFormat.getFormat("HH"); } return hour24DateTimeFormat; } private void appendTimelineBlocks(BlockRowData rowData, String style) { for (Entry<String, Element> entry : rowData.getBlockEntries()) { getElement().appendChild(entry.getValue()); } if (isAlwaysCalculatePixelWidths()) { getElement().appendChild(createSpacerBlock(style)); } } /** * Update horizontal overflow state. */ private void updateTimelineOverflowingHorizontally() { timelineOverflowingHorizontally = resolutionDiv.getClientWidth() > getElement().getParentElement() .getClientWidth(); } private DivElement createSpacerBlock(String className) { DivElement block = DivElement.as(DOM.createDiv()); block.setClassName(STYLE_ROW + " " + STYLE_YEAR); block.addClassName(STYLE_SPACER); block.setInnerText(" "); block.getStyle().setDisplay(Display.NONE); // not visible by default spacerBlocks.add(block); return block; } private void updateSpacerBlocks(double dayWidthPx) { double spaceLeft = resolutionDiv.getClientWidth() - (blocksInRange * dayWidthPx); if (spaceLeft > 0) { for (DivElement e : spacerBlocks) { e.getStyle().clearDisplay(); e.getStyle().setWidth(spaceLeft, Unit.PX); } resSpacerDiv = createResolutionBLock(); resSpacerDiv.addClassName(STYLE_SPACER); resSpacerDiv.getStyle().setWidth(spaceLeft, Unit.PX); resSpacerDiv.setInnerText(" "); resolutionDiv.appendChild(resSpacerDiv); } else { hideSpacerBlocks(); } } private void hideSpacerBlocks() { for (DivElement e : spacerBlocks) { e.getStyle().setDisplay(Display.NONE); } } private void updateBlockWidths(double dayWidthPercentage, double dayWidthPx, BlockRowData rowData) { int lastIndex; int i = 0; lastIndex = rowData.size() - 1; for (Entry<String, Element> entry : rowData.getBlockEntries()) { setWidth(blocksInRange, dayWidthPercentage, dayWidthPx, entry.getValue(), rowData.getBlockLength(entry.getKey())); ieFix(i, lastIndex, entry.getValue()); i++; } } private void updateResolutionBlockWidths(int resolutionBlockCount, double dayWidthPercentage, double dayWidthPx, double resBlockMinWidthPx, double resBlockWidthPx, double resBlockWidthPercentage, String pct) { boolean firstResBlockIsShort = firstResBlockCount > 0 && ((resolution == Resolution.Week && firstResBlockCount < DAYS_IN_WEEK)); boolean lastResBlockIsShort = lastResBlockCount > 0 && ((resolution == Resolution.Week && lastResBlockCount < DAYS_IN_WEEK)); int lastIndex = resolutionBlockCount - 1; int i; Element resBlock; for (i = 0; i < resolutionBlockCount; i++) { resBlock = Element.as(resolutionDiv.getChild(i)); // first and last week blocks may be thinner than other // resolution blocks. if (firstResBlockIsShort && i == 0) { setWidth(blocksInRange, dayWidthPercentage, dayWidthPx, resBlock, firstResBlockCount); } else if (lastResBlockIsShort && i == lastIndex) { setWidth(blocksInRange, dayWidthPercentage, dayWidthPx, resBlock, lastResBlockCount); } else { setWidth(resBlockMinWidthPx, resBlockWidthPercentage, resBlockWidthPx, pct, resBlock); } ieFix(i, lastIndex, resBlock); } } private void removeResolutionSpacerBlock() { if (resSpacerDiv != null && resSpacerDiv.hasParentElement()) { resSpacerDiv.removeFromParent(); } } private void prepareTimelineForHourResolution(long startDate, long endDate) { firstDay = true; prepareTimelineForResolution(HOUR_INTERVAL, startDate, endDate, new ResolutionBlockAdder() { int hourCounter = firstHourOfRange; @Override public void addResolutionBLock(int index, Date date, String currentYear, boolean lastTimelineBlock) { addHourResolutionBlock(date, index, hourCounter, lastTimelineBlock); hourCounter = Math.max((hourCounter + 1) % 25, 1); } }); } private void prepareTimelineForDayResolution(long startDate, long endDate) { firstWeek = true; prepareTimelineForResolution(DAY_INTERVAL, startDate, endDate, new ResolutionBlockAdder() { int dayCounter = firstDayOfRange; Weekday weekday; @Override public void addResolutionBLock(int index, Date date, String currentYear, boolean lastTimelineBlock) { weekday = getWeekday(dayCounter); if (resolution == Resolution.Week) { addWeekResolutionBlock(date, index, weekday, isWeekEnd(dayCounter), lastTimelineBlock); } else { addDayResolutionBlock(date, index, isWeekEnd(dayCounter)); } dayCounter = Math.max((dayCounter + 1) % 8, 1); } }); } private void prepareTimelineForResolution(long interval, long startDate, long endDate, ResolutionBlockAdder resBlockAdder) { this.interval = interval; blocksInRange = 0; even = false; firstResBlockCount = 0; lastResBlockCount = 0; String currentYear = null; String currentMonth = null; String currentDay = null; long pos = startDate; int index = 0; boolean lastTimelineBlock = false; Date date; while (pos <= endDate) { lastTimelineBlock = (pos + interval) > endDate; date = new Date(pos); resBlockAdder.addResolutionBLock(index, date, currentYear, lastTimelineBlock); if (isYearRowVisible()) { currentYear = addYearBlock(currentYear, date); } if (isMonthRowVisible()) { currentMonth = addMonthBlock(currentMonth, date); } if (isDayRowVisible()) { currentDay = addDayBlock(currentDay, date); } pos += interval; index++; } } private interface ResolutionBlockAdder { void addResolutionBLock(int index, Date date, String currentYear, boolean lastTimelineBlock); } public LocaleDataProvider getLocaleDataProvider() { return localeDataProvider; } private Weekday getWeekday(int dayCounter) { if (dayCounter == firstDayOfWeek) { return Weekday.First; } if (dayCounter == lastDayOfWeek) { return Weekday.Last; } return Weekday.Between; } private boolean isWeekEnd(int dayCounter) { return dayCounter == 1 || dayCounter == 7; } private String key(String prefix, BlockRowData rowData) { return prefix + "_" + (rowData.size()); } private String newKey(String prefix, BlockRowData rowData) { return prefix + "_" + (rowData.size() + 1); } private String addBlock(String current, String target, Date date, BlockRowData rowData, Operation operation) { String key; if (!target.equals(current)) { current = target; key = newKey("" + current, rowData); operation.run(target, key, date); } else { key = key("" + current, rowData); rowData.setBlockLength(key, rowData.getBlockLength(key) + 1); } return current; } private interface Operation { void run(String target, String value, Date date); } private String addDayBlock(String currentDay, Date date) { String day = getDay(date); return addBlock(currentDay, day, date, dayRowData, new Operation() { @Override public void run(String day, String key, Date date) { addDayBlock(key, formatDayCaption(day, date)); } }); } private String addMonthBlock(String currentMonth, Date date) { final int month = getMonth(date); return addBlock(currentMonth, String.valueOf(month), date, monthRowData, new Operation() { @Override public void run(String target, String key, Date date) { addMonthBlock(key, formatMonthCaption(month, date)); } }); } private String addYearBlock(String currentYear, Date date) { String year = getYear(date); return addBlock(currentYear, year, date, yearRowData, new Operation() { @Override public void run(String year, String key, Date date) { addYearBlock(key, formatYearCaption(year, date)); } }); } private void addMonthBlock(String key, String text) { DivElement monthBlock = createTimelineBlock(key, text, STYLE_MONTH, monthRowData); addEvenStyleIfNeeded(monthRowData.size(), monthBlock); } private void addYearBlock(String key, String text) { createTimelineBlock(key, text, STYLE_YEAR, yearRowData); } private void addDayBlock(String key, String text) { DivElement dayBlock = createTimelineBlock(key, text, STYLE_DAY, dayRowData); addEvenStyleIfNeeded(dayRowData.size(), dayBlock); } private DivElement createTimelineBlock(String key, String text, String styleSuffix, BlockRowData rowData) { DivElement div = DivElement.as(DOM.createDiv()); div.setClassName(STYLE_ROW + " " + styleSuffix); div.setInnerText(text); rowData.setBlockLength(key, 1); rowData.setBlock(key, div); return div; } private void addEvenStyleIfNeeded(int number, Element element) { if (ie8 && number % 2 == 0) { element.addClassName(STYLE_EVEN); } } private String formatDayCaption(String day, Date date) { if (dayFormat == null || dayFormat.isEmpty()) { return day; } return getLocaleDataProvider().formatDate(date, dayFormat); } private String formatYearCaption(String year, Date date) { if (yearFormat == null || yearFormat.isEmpty()) { return year; } return getLocaleDataProvider().formatDate(date, yearFormat); } private String formatWeekCaption(Date date) { if (weekFormat == null || weekFormat.isEmpty()) { return "" + getWeekNumber(date); } return getLocaleDataProvider().formatDate(date, weekFormat); } private String formatMonthCaption(int month, Date date) { if (monthFormat == null || monthFormat.isEmpty()) { return monthNames[month]; } return getLocaleDataProvider().formatDate(date, monthFormat); } private String getDay(Date date) { return getDayDateTimeFormat().format(date, gmt); } private String getYear(Date date) { return getYearDateTimeFormat().format(date, gmt); } private int getMonth(Date date) { String m = getMonthDateTimeFormat().format(date, gmt); return Integer.parseInt(m) - 1; } private String createCalcCssValue(int resolutionBlockCount) { return createCalcCssValue(resolutionBlockCount, null); } private String createCalcCssValue(int resolutionBlockCount, Integer multiplier) { if (ie) { // IEs up to 11 don't support more than two-decimal precision. // That's why we use calc(100% / x) or calc(123.12345%) css value to // workaround this limitation. // IE8 doesn't support calc() at all. if (!ie8) { if (multiplier != null) { double percents = 100.0 / resolutionBlockCount * multiplier.intValue(); return "calc(" + percents + "%)"; } return "calc(100% / " + resolutionBlockCount + ")"; } } return null; } private String createCalcCssValue(double v, double multiplier) { if (ie) { // see comments in createCalcCssValue(int, Integer) if (!ie8) { double percents = 100.0 / v * multiplier; return "calc(" + percents + "%)"; } } return null; } private void setWidth(int daysInRange, double dayWidthPercentage, double dayWidthPx, Element element, int position) { if (isTimelineOverflowingHorizontally()) { element.getStyle().setWidth(position * minResolutionWidth, Unit.PX); } else { if (isAlwaysCalculatePixelWidths()) { element.getStyle().setWidth(position * dayWidthPx, Unit.PX); } else { setCssPercentageWidth(element, daysInRange, dayWidthPercentage, position); } } } private void setWidth(double minPxValue, double pctValue, double pxValue, String pct, Element element) { if (isTimelineOverflowingHorizontally()) { element.getStyle().setWidth(minPxValue, Unit.PX); } else { if (isAlwaysCalculatePixelWidths()) { element.getStyle().setWidth(pxValue, Unit.PX); } else { setCssPercentageWidth(element, pctValue, pct); } } } private void ieFix(int index, int lastIndex, Element element) { if (!ie) { return; } if (index == lastIndex) { // last block if (ie9 || !"-ms-inline-flexbox".equals(element.getParentElement().getStyle().getProperty("display"))) { // IE9 workaround: adjust last block to have a -1px right // margin that helps to the lack of -ms-inline-flexbox // support. // Same workaround is used for all IEs when the parent element // don't have 'display: -ms-inline-flexbox' element.getStyle().setMarginRight(-1, Unit.PX); } } } private void setCssPercentageWidth(Element element, int daysInRange, double width, int position) { String pct = createCalcCssValue(daysInRange, position); setCssPercentageWidth(element, position * width, pct); } private void setCssPercentageWidth(Element element, double nValue, String pct) { if (pct != null) { element.getStyle().setProperty("width", pct); } else { element.getStyle().setWidth(nValue, Unit.PCT); } } private void addDayResolutionBlock(Date date, int index, boolean weekend) { DivElement resBlock = createResolutionBLock(); resBlock.setInnerText(getDayDateTimeFormat().format(date, gmt)); if (weekend) { resBlock.addClassName(STYLE_WEEKEND); } resolutionDiv.appendChild(resBlock); blocksInRange++; } private void addWeekResolutionBlock(Date date, int index, Weekday weekDay, boolean weekend, boolean lastBlock) { DivElement resBlock; if (index == 0 || weekDay == Weekday.First) { resBlock = createResolutionBLock(); resBlock.addClassName("w"); resBlock.setInnerText(formatWeekCaption(date)); if (index > 0) { even = !even; } if (even) { resBlock.addClassName(STYLE_EVEN); } // append just one week resolution block per week. resolutionDiv.appendChild(resBlock); } if (firstWeek && (weekDay == Weekday.Last || lastBlock)) { firstWeek = false; firstResBlockCount = index + 1; } else if (lastBlock) { lastResBlockCount = (index + 1 - firstResBlockCount) % 7; } blocksInRange++; } private void addHourResolutionBlock(Date date, int index, int hourCounter, boolean lastBlock) { DivElement resBlock; resBlock = createResolutionBLock(); resBlock.addClassName("h"); if (getLocaleDataProvider().isTwelveHourClock()) { resBlock.setInnerText(getHour12DateTimeFormat().format(date, gmt)); } else { resBlock.setInnerText(getHour24DateTimeFormat().format(date, gmt)); } if (index > 0) { even = !even; } if (even) { resBlock.addClassName(STYLE_EVEN); } resolutionDiv.appendChild(resBlock); if (firstDay && (hourCounter == 25 || lastBlock)) { firstDay = false; firstResBlockCount = index + 1; } else if (lastBlock) { lastResBlockCount = (index + 1 - firstResBlockCount) % 24; } blocksInRange++; } private DivElement createResolutionBLock() { DivElement resBlock = DivElement.as(DOM.createDiv()); resBlock.setClassName("col"); return resBlock; } private boolean isChanged(Resolution resolution, long startDate, long endDate, int firstDayOfWeek, int firstDayOfRange, int firstHourOfRange, String locale) { boolean resolutionChanged = this.resolution != resolution; if (resolutionChanged) { minResolutionWidth = -1; } if (forceUpdateFlag) { forceUpdateFlag = false; return true; } return resolutionChanged || this.startDate != startDate || this.endDate != endDate || this.firstDayOfWeek != firstDayOfWeek || this.firstDayOfRange != firstDayOfRange || this.firstHourOfRange != firstHourOfRange || (this.locale == null && locale != null || (this.locale != null && !this.locale.equals(locale))); } private int calculateResolutionMinWidth() { if (resolution == Resolution.Week) { return RESOLUTION_WEEK_DAYBLOCK_WIDTH; } boolean removeResolutionDiv = false; if (!resolutionDiv.hasParentElement()) { removeResolutionDiv = true; getElement().appendChild(resolutionDiv); } DivElement resBlockMeasure = DivElement.as(DOM.createDiv()); resBlockMeasure.setInnerText("MM"); resBlockMeasure.setClassName(STYLE_COL); resolutionDiv.appendChild(resBlockMeasure); int width = resBlockMeasure.getClientWidth(); resBlockMeasure.removeFromParent(); if (removeResolutionDiv) { resolutionDiv.removeFromParent(); } return width; } private void clear() { while (getElement().hasChildNodes()) { getElement().getLastChild().removeFromParent(); } spacerBlocks.clear(); yearRowData.clear(); monthRowData.clear(); dayRowData.clear(); } public static int getWeekNumber(Date d) { /* * Thanks to stackoverflow.com for a easy function to calculate week * number. See * http://stackoverflow.com/questions/6117814/get-week-of-year * -in-javascript-like-in-php */ d = new Date(d.getTime()); d.setHours(0); d.setDate(d.getDate() + 4 - (d.getDay() % 7)); Date yearStart = new Date(d.getYear(), 0, 1); double weekNo = Math.ceil((((d.getTime() - yearStart.getTime()) / 86400000) + 1.0) / 7.0); return (int) weekNo; } private class BlockRowData { private final Map<String, Element> blocks = new LinkedHashMap<String, Element>(); private final Map<String, Integer> blockLength = new LinkedHashMap<String, Integer>(); public int size() { return blocks.size(); } public Element getBlock(String key) { return blocks.get(key); } public Set<Entry<String, Element>> getBlockEntries() { return blocks.entrySet(); } public void setBlock(String key, Element element) { blocks.put(key, element); } public Integer getBlockLength(String key) { return blockLength.get(key); } public void setBlockLength(String key, Integer length) { blockLength.put(key, length); } public void clear() { blocks.clear(); blockLength.clear(); } } }