/**
* Copyright 2010-2013 Jingjing Li.
*
* This file is part of jplot2d.
*
* jplot2d is free software: you can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or any later version.
*
* jplot2d 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 Lesser Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with jplot2d. If not, see .
*/
package org.jplot2d.axtick;
import java.lang.reflect.Array;
import java.text.Format;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Formatter;
import java.util.List;
import java.util.Locale;
import java.util.TimeZone;
import org.jplot2d.axtick.DateInterval.Unit;
import org.jplot2d.tex.MathElement;
import org.jplot2d.tex.TeXMathUtils;
/**
*
* @author Jingjing Li
*
*/
public class DateTickCalculator extends LongTickCalculator implements RangeAdvisor {
private final TimeZone zone;
private final Locale locale;
/**
* Always be positive
*/
private DateInterval dateInterval;
private int minorNumber;
private long[] tickValues;
private long[] minorValues;
public DateTickCalculator(TimeZone zone, Locale locale) {
this.zone = zone;
this.locale = locale;
}
protected static DateInterval calcInterval(long start, long end, int tickNumber) {
if (start == end) {
throw new IllegalArgumentException("The range span must be great than zero");
}
if (tickNumber <= 0) {
throw new IllegalArgumentException("The ticks number must be great than zero");
} else if (tickNumber == 1) {
tickNumber = 2;
}
long lo, hi;
if (end > start) {
lo = start;
hi = end;
} else {
lo = end;
hi = start;
}
tickNumber = Math.abs(tickNumber);
double rough = (double) (hi - lo) / (tickNumber - 1);
DateInterval[] itvAB = calcCandidateIntervals(rough);
DateInterval itvA = itvAB[0];
DateInterval itvB = itvAB[1];
long iLoA = lo / itvA.getTimeInMillis();
long iHiA = hi / itvA.getTimeInMillis();
if (hi % itvA.getTimeInMillis() != 0) {
iHiA++;
}
int tickNumA = (int) (iHiA - iLoA + 1);
long iLoB = lo / itvB.getTimeInMillis();
long iHiB = hi / itvB.getTimeInMillis();
if (hi % itvB.getTimeInMillis() != 0) {
iHiB++;
}
int tickNumB = (int) (iHiB - iLoB + 1);
/* tickNumB < tickNumber < tickNumA */
if (tickNumA - tickNumber <= tickNumber - tickNumB) {
return itvA;
} else {
return itvB;
}
}
/**
* Calculate 2 candidate DateInterval based on rough interval in milliseconds
*
* @param interval
* the rough interval in milliseconds
* @return 2 candidate DateInterval in a array
*/
protected static DateInterval[] calcCandidateIntervals(double rough) {
DateInterval itvA, itvB;
if (rough < 1000) {
int scale;
if (rough < 10) {
scale = 1;
} else if (rough < 100) {
scale = 10;
} else {
scale = 100;
}
/* 1 <= rough/scale < 10 */
double coeff = rough / scale;
if (coeff < 2) {
itvA = new DateInterval(Unit.MILLISECOND, scale * 1);
itvB = new DateInterval(Unit.MILLISECOND, scale * 2);
} else if (coeff < 5) {
itvA = new DateInterval(Unit.MILLISECOND, scale * 2);
itvB = new DateInterval(Unit.MILLISECOND, scale * 5);
} else {
itvA = new DateInterval(Unit.MILLISECOND, scale * 5);
itvB = new DateInterval(Unit.MILLISECOND, scale * 10);
}
} else if (rough < Unit.SECOND.time * 2) {
itvA = new DateInterval(Unit.SECOND, 1);
itvB = new DateInterval(Unit.SECOND, 2);
} else if (rough < Unit.SECOND.time * 5) {
itvA = new DateInterval(Unit.SECOND, 2);
itvB = new DateInterval(Unit.SECOND, 5);
} else if (rough < Unit.SECOND.time * 10) {
itvA = new DateInterval(Unit.SECOND, 5);
itvB = new DateInterval(Unit.SECOND, 10);
} else if (rough < Unit.SECOND.time * 15) {
itvA = new DateInterval(Unit.SECOND, 10);
itvB = new DateInterval(Unit.SECOND, 15);
} else if (rough < Unit.SECOND.time * 30) {
itvA = new DateInterval(Unit.SECOND, 15);
itvB = new DateInterval(Unit.SECOND, 30);
} else if (rough < Unit.MINUTE.time) {
itvA = new DateInterval(Unit.SECOND, 30);
itvB = new DateInterval(Unit.MINUTE, 1);
} else if (rough < Unit.MINUTE.time * 2) {
itvA = new DateInterval(Unit.MINUTE, 1);
itvB = new DateInterval(Unit.MINUTE, 2);
} else if (rough < Unit.MINUTE.time * 5) {
itvA = new DateInterval(Unit.MINUTE, 2);
itvB = new DateInterval(Unit.MINUTE, 5);
} else if (rough < Unit.MINUTE.time * 10) {
itvA = new DateInterval(Unit.MINUTE, 5);
itvB = new DateInterval(Unit.MINUTE, 10);
} else if (rough < Unit.MINUTE.time * 15) {
itvA = new DateInterval(Unit.MINUTE, 10);
itvB = new DateInterval(Unit.MINUTE, 15);
} else if (rough < Unit.MINUTE.time * 30) {
itvA = new DateInterval(Unit.MINUTE, 15);
itvB = new DateInterval(Unit.MINUTE, 30);
} else if (rough < Unit.HOUR.time) {
itvA = new DateInterval(Unit.MINUTE, 30);
itvB = new DateInterval(Unit.HOUR, 1);
} else if (rough < Unit.HOUR.time * 2) {
itvA = new DateInterval(Unit.HOUR, 1);
itvB = new DateInterval(Unit.HOUR, 2);
} else if (rough < Unit.HOUR.time * 3) {
itvA = new DateInterval(Unit.HOUR, 2);
itvB = new DateInterval(Unit.HOUR, 3);
} else if (rough < Unit.HOUR.time * 6) {
itvA = new DateInterval(Unit.HOUR, 3);
itvB = new DateInterval(Unit.HOUR, 6);
} else if (rough < Unit.HOUR.time * 12) {
itvA = new DateInterval(Unit.HOUR, 6);
itvB = new DateInterval(Unit.HOUR, 12);
} else if (rough < Unit.DAY.time) {
itvA = new DateInterval(Unit.HOUR, 12);
itvB = new DateInterval(Unit.DAY, 1);
} else if (rough < Unit.DAY.time * 2) {
itvA = new DateInterval(Unit.DAY, 1);
itvB = new DateInterval(Unit.DAY, 2);
} else if (rough < Unit.WEEK.time) {
itvA = new DateInterval(Unit.DAY, 2);
itvB = new DateInterval(Unit.WEEK, 1);
} else if (rough < Unit.WEEK.time * 2) {
itvA = new DateInterval(Unit.WEEK, 1);
itvB = new DateInterval(Unit.WEEK, 2);
} else if (rough < Unit.MONTH.time) {
itvA = new DateInterval(Unit.WEEK, 2);
itvB = new DateInterval(Unit.MONTH, 1);
} else if (rough < Unit.MONTH.time * 3) {
itvA = new DateInterval(Unit.MONTH, 1);
itvB = new DateInterval(Unit.MONTH, 3);
} else if (rough < Unit.MONTH.time * 6) {
itvA = new DateInterval(Unit.MONTH, 3);
itvB = new DateInterval(Unit.MONTH, 6);
} else if (rough < Unit.YEAR.time * 1) {
itvA = new DateInterval(Unit.MONTH, 6);
itvB = new DateInterval(Unit.YEAR, 1);
} else {
double roughYear = rough / Unit.YEAR.time;
int expn = (int) Math.floor(Math.log10(roughYear));
int scale = (int) Math.pow(10, expn);
/* 1 <= rough/scale < 10 */
double coeff = roughYear / scale;
if (coeff < 2) {
itvA = new DateInterval(Unit.YEAR, scale * 1);
itvB = new DateInterval(Unit.YEAR, scale * 2);
} else if (coeff < 5) {
itvA = new DateInterval(Unit.YEAR, scale * 2);
itvB = new DateInterval(Unit.YEAR, scale * 5);
} else {
itvA = new DateInterval(Unit.YEAR, scale * 5);
itvB = new DateInterval(Unit.YEAR, scale * 10);
}
}
return new DateInterval[] { itvA, itvB };
}
/**
* @param interval
* must be positive
* @param offset
* @param minorTickNumber
*/
protected void calcValues() {
if (dateInterval.getValue() == 0)
throw new IllegalArgumentException("delta cannot be zero");
Calendar loCal = Calendar.getInstance(zone, locale);
loCal.setTimeInMillis(lo);
Calendar hiCal = Calendar.getInstance(zone, locale);
hiCal.setTimeInMillis(hi);
Calendar t1cal = (Calendar) loCal.clone();
/* round up to unit */
setCalendarBelowToMin(t1cal, dateInterval.getUnit());
if (t1cal.before(loCal)) {
add(t1cal, dateInterval.getUnit(), 1);
}
List ticks = new ArrayList();
List mticks = new ArrayList();
if (minorNumber == 0) {
int delta = distanceToIntervalBoundary(t1cal, dateInterval);
if (delta != 0) {
add(t1cal, dateInterval.getUnit(), dateInterval.getValue() - delta);
}
while (!t1cal.after(hiCal)) {
// System.out.println(t1cal.getTimeInMillis());
ticks.add(t1cal.getTimeInMillis());
add(t1cal, dateInterval.getUnit(), dateInterval.getValue());
}
} else {
int mitv = dateInterval.getValue() / (minorNumber + 1);
DateInterval minorInterval = new DateInterval(dateInterval.getUnit(), mitv);
int delta = distanceToIntervalBoundary(t1cal, minorInterval);
if (delta != 0) {
add(t1cal, dateInterval.getUnit(), mitv - delta);
}
while (!t1cal.after(hiCal)) {
if (distanceToIntervalBoundary(t1cal, dateInterval) == 0) {
ticks.add(t1cal.getTimeInMillis());
} else {
mticks.add(t1cal.getTimeInMillis());
}
add(t1cal, dateInterval.getUnit(), mitv);
}
}
tickValues = new long[ticks.size()];
minorValues = new long[mticks.size()];
if (inverted) {
for (int i = 0, j = tickValues.length - 1; i < tickValues.length; i++, j--) {
tickValues[i] = ticks.get(j);
}
for (int i = 0, j = minorValues.length - 1; i < minorValues.length; i++, j--) {
minorValues[i] = mticks.get(j);
}
} else {
for (int i = 0; i < tickValues.length; i++) {
tickValues[i] = ticks.get(i);
}
for (int i = 0; i < minorValues.length; i++) {
minorValues[i] = mticks.get(i);
}
}
}
public void expandRangeByTickNumber(int tickNumber) {
if (tickNumber <= 0) {
throw new IllegalArgumentException("tick number must be positive.");
} else if (tickNumber == 1) {
tickNumber = 2;
}
long span = hi - lo;
if (span < tickNumber - 1) {
/* expand range to tick number */
long halfXpand = (tickNumber - 1 - span) / 2;
long odd = (tickNumber - 1 - span) % 2;
lo -= halfXpand;
hi += halfXpand;
hi += odd;
if (lo < 0) {
lo = 0;
hi -= lo;
}
dateInterval = new DateInterval(Unit.MILLISECOND, 1);
} else {
dateInterval = calcInterval(lo, hi, tickNumber);
expandRangeByTickInterval();
}
}
public void expandRangeByTickInterval(double interval) {
dateInterval = DateInterval.createWithMillis((long) Math.round(interval));
expandRangeByTickInterval();
/* if _lo == _hi and on interval boundary, the expanding has no effect */
if (lo == hi) {
Calendar hiCal = Calendar.getInstance(zone, locale);
hiCal.setTimeInMillis(hi);
add(hiCal, dateInterval.getUnit(), dateInterval.getValue());
hi = hiCal.getTimeInMillis();
}
}
protected void expandRangeByTickInterval() {
Calendar loCal = Calendar.getInstance(zone, locale);
loCal.setTimeInMillis(lo);
Calendar hiCal = Calendar.getInstance(zone, locale);
hiCal.setTimeInMillis(hi);
setCalendarBelowToMin(loCal, dateInterval.getUnit());
int loDelta = distanceToIntervalBoundary(loCal, dateInterval);
if (loDelta != 0) {
add(loCal, dateInterval.getUnit(), -loDelta);
}
setCalendarBelowToMin(hiCal, dateInterval.getUnit());
if (hiCal.getTimeInMillis() < hi) {
add(hiCal, dateInterval.getUnit(), 1);
}
int hiDelta = distanceToIntervalBoundary(hiCal, dateInterval);
if (hiDelta != 0) {
add(hiCal, dateInterval.getUnit(), dateInterval.getValue() - hiDelta);
}
lo = loCal.getTimeInMillis();
hi = hiCal.getTimeInMillis();
}
public void calcValuesByTickNumber(int tickNumber, int minorTickNumber) {
DateInterval interval = calcInterval(lo, hi, tickNumber);
calcValuesByTickInterval(interval, 0, minorTickNumber);
}
public void calcValuesByTickInterval(long interval, long offset, int minorTickNumber) {
calcValuesByTickInterval(DateInterval.createWithMillis(interval), offset, minorTickNumber);
}
private void calcValuesByTickInterval(DateInterval interval, long offset, int minorTickNumber) {
dateInterval = interval;
if (dateInterval.getValue() == 1) {
minorNumber = 0;
} else {
if (minorTickNumber == AUTO_MINORTICK_NUMBER) {
minorTickNumber = 3;
}
minorNumber = calcMinorNumber(dateInterval.getValue(), minorTickNumber);
}
calcValues();
}
public double getInterval() {
return dateInterval.getTimeInMillis();
}
public int getMinorNumber() {
return minorNumber;
}
public long[] getValues() {
return tickValues;
}
public long[] getMinorValues() {
return minorValues;
}
public String getLabelFormate() {
Calendar loCal = Calendar.getInstance(zone, locale);
loCal.setTimeInMillis(lo);
Calendar hiCal = Calendar.getInstance(zone, locale);
hiCal.setTimeInMillis(hi);
Unit umax = getFirsNonEqualField(loCal, hiCal);
return calcLabelFormat(dateInterval.getUnit(), umax);
}
public Format calcLabelTextFormat(Object canonicalValues) {
return null;
}
public String calcLabelFormatString(Object values) {
if (((long[]) values).length == 0) {
return "";
}
Calendar cal = Calendar.getInstance(zone, locale);
Unit umin = Unit.YEAR;
long lo = Long.MAX_VALUE, hi = 0;
for (long v : (long[]) values) {
if (lo > v) {
lo = v;
}
if (hi < v) {
hi = v;
}
cal.setTimeInMillis(v);
Unit lnmf = getLastNonMinField(cal);
if (umin.time > lnmf.time) {
umin = lnmf;
}
}
Calendar loCal = Calendar.getInstance(zone, locale);
loCal.setTimeInMillis(lo);
Calendar hiCal = Calendar.getInstance(zone, locale);
hiCal.setTimeInMillis(hi);
Unit umax = getFirsNonEqualField(loCal, hiCal);
return calcLabelFormat(umin, umax);
}
public boolean isValidFormat(String format) {
try {
String.format(format, Calendar.getInstance(zone, locale));
return true;
} catch (IllegalArgumentException e) {
return false;
}
}
protected static String calcLabelFormat(Unit uprec, Unit umdiff) {
if (uprec.time > umdiff.time) {
throw new IllegalArgumentException("The MaxDiff unit must larger than precision unit.");
}
/* format string for hh:mm:ss.mmm without the % char */
String hms;
switch (uprec) {
case MICROSECOND:
hms = "tT.%= Unit.DAY.time) {
return "%tF %<" + hms;
} else {
return "%" + hms;
}
}
protected static Unit getLastNonMinField(Calendar cal) {
if (cal.get(Calendar.MILLISECOND) > cal.getMinimum(Calendar.MILLISECOND)) {
return Unit.MILLISECOND;
}
if (cal.get(Calendar.SECOND) > cal.getMinimum(Calendar.SECOND)) {
return Unit.SECOND;
}
if (cal.get(Calendar.MINUTE) > cal.getMinimum(Calendar.MINUTE)) {
return Unit.MINUTE;
}
if (cal.get(Calendar.HOUR_OF_DAY) > cal.getMinimum(Calendar.HOUR_OF_DAY)) {
return Unit.HOUR;
}
if (cal.get(Calendar.DAY_OF_MONTH) > cal.getMinimum(Calendar.DAY_OF_MONTH)) {
return Unit.DAY;
}
if (cal.get(Calendar.MONTH) > cal.getMinimum(Calendar.MONTH)) {
return Unit.MONTH;
}
if (cal.get(Calendar.YEAR) > cal.getMinimum(Calendar.YEAR)) {
return Unit.YEAR;
}
return null;
}
protected static Unit getFirsNonEqualField(Calendar a, Calendar b) {
if (a.get(Calendar.YEAR) != b.get(Calendar.YEAR)) {
return Unit.YEAR;
}
if (a.get(Calendar.MONTH) != b.get(Calendar.MONTH)) {
return Unit.MONTH;
}
if (a.get(Calendar.DAY_OF_MONTH) != b.get(Calendar.DAY_OF_MONTH)) {
return Unit.DAY;
}
if (a.get(Calendar.HOUR_OF_DAY) != b.get(Calendar.HOUR_OF_DAY)) {
return Unit.HOUR;
}
if (a.get(Calendar.MINUTE) != b.get(Calendar.MINUTE)) {
return Unit.MINUTE;
}
if (a.get(Calendar.SECOND) != b.get(Calendar.SECOND)) {
return Unit.SECOND;
}
if (a.get(Calendar.MILLISECOND) != b.get(Calendar.MILLISECOND)) {
return Unit.MILLISECOND;
}
return null;
}
/**
* Sets all calendar fields below the given unit to its minimal value.
*
* @param cal
* @param unit
* @return true if any fields below the given unit is larger than its minimal value
*/
protected static void setCalendarBelowToMin(Calendar cal, Unit unit) {
switch (unit) {
case MILLISECOND:
break;
case SECOND:
cal.set(Calendar.MILLISECOND, cal.getMinimum(Calendar.MILLISECOND));
break;
case MINUTE:
cal.set(Calendar.MILLISECOND, cal.getMinimum(Calendar.MILLISECOND));
cal.set(Calendar.SECOND, cal.getMinimum(Calendar.SECOND));
break;
case HOUR:
cal.set(Calendar.MILLISECOND, cal.getMinimum(Calendar.MILLISECOND));
cal.set(Calendar.SECOND, cal.getMinimum(Calendar.SECOND));
cal.set(Calendar.MINUTE, cal.getMinimum(Calendar.MINUTE));
break;
case DAY:
cal.set(Calendar.MILLISECOND, cal.getMinimum(Calendar.MILLISECOND));
cal.set(Calendar.SECOND, cal.getMinimum(Calendar.SECOND));
cal.set(Calendar.MINUTE, cal.getMinimum(Calendar.MINUTE));
cal.set(Calendar.HOUR_OF_DAY, cal.getMinimum(Calendar.HOUR_OF_DAY));
break;
case WEEK:
cal.set(Calendar.MILLISECOND, cal.getMinimum(Calendar.MILLISECOND));
cal.set(Calendar.SECOND, cal.getMinimum(Calendar.SECOND));
cal.set(Calendar.MINUTE, cal.getMinimum(Calendar.MINUTE));
cal.set(Calendar.HOUR_OF_DAY, cal.getMinimum(Calendar.HOUR_OF_DAY));
int woy = cal.get(Calendar.WEEK_OF_YEAR);
cal.set(Calendar.DAY_OF_WEEK, cal.getMinimum(Calendar.DAY_OF_WEEK));
cal.set(Calendar.WEEK_OF_YEAR, woy);
break;
case MONTH:
cal.set(Calendar.MILLISECOND, cal.getMinimum(Calendar.MILLISECOND));
cal.set(Calendar.SECOND, cal.getMinimum(Calendar.SECOND));
cal.set(Calendar.MINUTE, cal.getMinimum(Calendar.MINUTE));
cal.set(Calendar.HOUR_OF_DAY, cal.getMinimum(Calendar.HOUR_OF_DAY));
cal.set(Calendar.DAY_OF_MONTH, cal.getMinimum(Calendar.DAY_OF_MONTH));
break;
case YEAR:
cal.set(Calendar.MILLISECOND, cal.getMinimum(Calendar.MILLISECOND));
cal.set(Calendar.SECOND, cal.getMinimum(Calendar.SECOND));
cal.set(Calendar.MINUTE, cal.getMinimum(Calendar.MINUTE));
cal.set(Calendar.HOUR_OF_DAY, cal.getMinimum(Calendar.HOUR_OF_DAY));
cal.set(Calendar.DAY_OF_YEAR, cal.getMinimum(Calendar.DAY_OF_YEAR));
break;
}
}
protected static void add(Calendar cal, Unit unit, int amount) {
switch (unit) {
case MILLISECOND:
cal.add(Calendar.MILLISECOND, amount);
break;
case SECOND:
cal.add(Calendar.SECOND, amount);
break;
case MINUTE:
cal.add(Calendar.MINUTE, amount);
break;
case HOUR:
cal.add(Calendar.HOUR_OF_DAY, amount);
break;
case DAY:
cal.add(Calendar.DAY_OF_MONTH, amount);
break;
case WEEK:
cal.add(Calendar.WEEK_OF_YEAR, amount);
break;
case MONTH:
cal.add(Calendar.MONTH, amount);
break;
case YEAR:
cal.add(Calendar.YEAR, amount);
break;
}
}
/**
* Returns the distance downward to a interval boundary.
*
* @param cal
* @param itv
* @return the distance.
*/
protected static int distanceToIntervalBoundary(Calendar cal, DateInterval itv) {
int v = 0, min = 0;
switch (itv.getUnit()) {
case MILLISECOND:
v = cal.get(Calendar.MILLISECOND);
min = cal.getMinimum(Calendar.MILLISECOND);
break;
case SECOND:
v = cal.get(Calendar.SECOND);
min = cal.getMinimum(Calendar.SECOND);
break;
case MINUTE:
v = cal.get(Calendar.MINUTE);
min = cal.getMinimum(Calendar.MINUTE);
break;
case HOUR:
v = cal.get(Calendar.HOUR_OF_DAY);
min = cal.getMinimum(Calendar.HOUR_OF_DAY);
break;
case DAY:
v = cal.get(Calendar.DAY_OF_MONTH);
min = cal.getMinimum(Calendar.DAY_OF_MONTH);
break;
case WEEK:
v = cal.get(Calendar.WEEK_OF_YEAR);
min = cal.getMinimum(Calendar.WEEK_OF_YEAR);
break;
case MONTH:
v = cal.get(Calendar.MONTH);
min = cal.getMinimum(Calendar.MONTH);
break;
case YEAR:
v = cal.get(Calendar.YEAR);
min = 0;
break;
}
int delta = (v - min) % itv.getValue();
return delta;
}
public MathElement[] formatValues(String format, Object values) {
Calendar cal = Calendar.getInstance(zone, locale);
int n = Array.getLength(values);
MathElement[] labels = new MathElement[n];
for (int i = 0; i < n; i++) {
cal.setTimeInMillis(Array.getLong(values, i));
@SuppressWarnings("resource")
String texString = new Formatter(locale).format(format, cal).toString();
if (texString.indexOf('$') == -1) {
labels[i] = new MathElement.Mtext(texString);
} else {
labels[i] = TeXMathUtils.parseText(texString);
}
}
return labels;
}
}
Ads help maintain this website.