/*******************************************************************************
* Copyright (c) 2012, 2017 Diamond Light Source Ltd.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
******************************************************************************/
package org.eclipse.nebula.visualization.xygraph.linearscale;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;
import java.util.List;
import org.eclipse.draw2d.FigureUtilities;
import org.eclipse.draw2d.geometry.Dimension;
/**
* Default scale tick mark algorithm
*
*/
public class LinearScaleTicks implements ITicksProvider {
/**
* The name of this tick provider
*/
public static final String NAME = "DEFAULT";
private static final String MINUS = "-";
private static final int TICK_LABEL_GAP = 2;
/**
* Get base^exponent
*/
private BigDecimal pow(double base, int exponent) {
BigDecimal result;
if (exponent >= 0) {
result = BigDecimal.valueOf(base).pow(exponent);
} else {
result = BigDecimal.ONE.divide(BigDecimal.valueOf(base).pow(-exponent));
}
return result;
}
/** default: show max label */
private boolean showMaxLabel = true;
/** default: show min label */
private boolean showMinLabel = true;
/** the array of tick label vales */
private ArrayList<Double> tickLabelValues;
/** the array of tick label */
private ArrayList<String> tickLabels;
/** the array of tick label position in pixels */
private ArrayList<Integer> tickLabelPositions;
/** the array of visibility state of tick label */
private ArrayList<Boolean> tickLabelVisibilities;
/** the maximum length of tick labels */
private int tickLabelMaxLength;
/** the maximum height of tick labels */
private int tickLabelMaxHeight;
private int gridStepInPixel;
/** the array of minor tick positions in pixels */
private ArrayList<Integer> minorPositions;
private IScaleProvider scale;
/**
* constructor
*
* @param scale
*/
public LinearScaleTicks(IScaleProvider scale) {
this.scale = scale;
tickLabelValues = new ArrayList<Double>();
tickLabels = new ArrayList<String>();
tickLabelPositions = new ArrayList<Integer>();
tickLabelVisibilities = new ArrayList<Boolean>();
minorPositions = new ArrayList<Integer>();
}
/**
* @return the gridStepInPixel
*/
public int getGridStepInPixels() {
return gridStepInPixel;
}
/**
* @return the tickLabelMaxHeight
*/
public int getTickLabelMaxHeight() {
return tickLabelMaxHeight;
}
/**
* @return the tickLabelMaxLength
*/
public int getTickLabelMaxLength() {
return tickLabelMaxLength;
}
@Override
public List<Integer> getPositions() {
return tickLabelPositions;
}
@Override
public List<Boolean> getVisibilities() {
return tickLabelVisibilities;
}
@Override
public int getPosition(int index) {
return tickLabelPositions.get(index);
}
@Override
public double getValue(int index) {
return tickLabelValues.get(index);
}
@Override
public String getLabel(int index) {
return tickLabels.get(index);
}
@Override
public List<String> getLabels() {
return tickLabels;
}
@Override
public int getLabelPosition(int index) {
return tickLabelPositions.get(index);
}
@Override
public boolean isVisible(int index) {
return tickLabelVisibilities.get(index);
}
@Override
public int getMajorCount() {
return tickLabels.size();
}
@Override
public int getMinorCount() {
return minorPositions.size();
}
@Override
public int getMinorPosition(int index) {
return minorPositions.get(index);
}
@Override
public int getMaxWidth() {
return tickLabelMaxLength;
}
@Override
public int getMaxHeight() {
return tickLabelMaxHeight;
}
@Override
public boolean isShowMaxLabel() {
return showMaxLabel;
}
@Override
public void setShowMaxLabel(boolean showMaxLabel) {
this.showMaxLabel = showMaxLabel;
}
@Override
public boolean isShowMinLabel() {
return showMinLabel;
}
@Override
public void setShowMinLabel(boolean showMinLabel) {
this.showMinLabel = showMinLabel;
}
/**
* Gets the grid step.
*
* @param lengthInPixels
* scale length in pixels
* @param min
* minimum value
* @param max
* maximum value
* @return rounded value.
*/
private double getGridStep(int lengthInPixels, double min, double max) {
if ((int) scale.getMajorGridStep() != 0) {
return scale.getMajorGridStep();
}
if (lengthInPixels <= 0) {
lengthInPixels = 1;
}
boolean minBigger = false;
if (min >= max) {
if (max == min)
max++;
else {
minBigger = true;
double swap = min;
min = max;
max = swap;
}
}
double length = Math.abs(max - min);
double majorTickMarkStepHint = scale.getMajorTickMarkStepHint();
if (majorTickMarkStepHint > lengthInPixels)
majorTickMarkStepHint = lengthInPixels;
double gridStepHint = length / lengthInPixels * majorTickMarkStepHint;
if (scale.isDateEnabled()) {
double temp = getTimeGridStep(min, max, gridStepHint);
if (minBigger)
temp = -temp;
return temp;
}
double mantissa = gridStepHint;
int exp = 0;
if (mantissa < 1) {
if (mantissa != 0)
while (mantissa < 1) {
mantissa *= 10.0;
exp--;
}
} else {
while (mantissa >= 10) {
mantissa /= 10.0;
exp++;
}
}
double gridStep;
if (mantissa > 7.5) {
// 10*10^exp
gridStep = 10 * Math.pow(10, exp);
} else if (mantissa > 3.5) {
// 5*10^exp
gridStep = 5 * Math.pow(10, exp);
} else if (mantissa > 1.5) {
// 2.0*10^exp
gridStep = 2 * Math.pow(10, exp);
} else {
gridStep = Math.pow(10, exp); // 1*10^exponent
}
if (minBigger)
gridStep = -gridStep;
return gridStep;
}
/**
* Given min, max and the gridStepHint, returns the time grid step as a
* double.
*
* @param min
* minimum value
* @param max
* maximum value
* @param gridStepHint
* @return time rounded value
*/
private double getTimeGridStep(double min, double max, double gridStepHint) {
// by default, make the least step to be minutes
long timeStep;
if (max - min < 1000) // <1 sec, step = 10 ms
timeStep = 10l;
else if (max - min < 60000) // < 1 min, step = 1 sec
timeStep = 1000l;
else if (max - min < 600000) // < 10 min, step = 10 sec
timeStep = 10000l;
else if (max - min < 6400000) // < 2 hour, step = 1 min
timeStep = 60000l;
else if (max - min < 43200000) // < 12 hour, step = 10 min
timeStep = 600000l;
else if (max - min < 86400000) // < 24 hour, step = 30 min
timeStep = 1800000l;
else if (max - min < 604800000) // < 7 days, step = 1 hour
timeStep = 3600000l;
else
timeStep = 86400000l;
if (scale.getTimeUnit() == Calendar.SECOND) {
timeStep = 1000l;
} else if (scale.getTimeUnit() == Calendar.MINUTE) {
timeStep = 60000l;
} else if (scale.getTimeUnit() == Calendar.HOUR_OF_DAY) {
timeStep = 3600000l;
} else if (scale.getTimeUnit() == Calendar.DATE) {
timeStep = 86400000l;
} else if (scale.getTimeUnit() == Calendar.MONTH) {
timeStep = 30l * 86400000l;
} else if (scale.getTimeUnit() == Calendar.YEAR) {
timeStep = 365l * 86400000l;
}
double temp = gridStepHint + (timeStep - gridStepHint % timeStep);
return temp;
}
/**
* If it has enough space to draw the tick label
*/
private boolean hasSpaceToDraw(int previousPosition, int tickLabelPosition, String previousTickLabel,
String tickLabel) {
Dimension tickLabelSize = FigureUtilities.getTextExtents(tickLabel, scale.getFont());
Dimension previousTickLabelSize = FigureUtilities.getTextExtents(previousTickLabel, scale.getFont());
int interval = tickLabelPosition - previousPosition;
int textLength = (int) (scale.isHorizontal() ? (tickLabelSize.width / 2.0 + previousTickLabelSize.width / 2.0)
: tickLabelSize.height);
boolean noLapOnPrevoius = true;
boolean noLapOnEnd = true;
// if it is not the end tick label
if (tickLabelPosition != tickLabelPositions.get(tickLabelPositions.size() - 1)) {
noLapOnPrevoius = interval > (textLength + TICK_LABEL_GAP);
Dimension endTickLabelSize = FigureUtilities.getTextExtents(tickLabels.get(tickLabels.size() - 1),
scale.getFont());
interval = tickLabelPositions.get(tickLabelPositions.size() - 1) - tickLabelPosition;
textLength = (int) (scale.isHorizontal() ? (tickLabelSize.width / 2.0 + endTickLabelSize.width / 2.0)
: tickLabelSize.height);
noLapOnEnd = interval > textLength + TICK_LABEL_GAP;
}
return noLapOnPrevoius && noLapOnEnd;
}
/**
* Checks if the tick label is major tick. For example: 0.001, 0.01, 0.1, 1,
* 10, 100...
*/
private boolean isMajorTick(double tickValue) {
if (!scale.isLogScaleEnabled()) {
return true;
}
double log10 = Math.log10(tickValue);
if (log10 == Math.rint(log10)) {
return true;
}
return false;
}
@Override
public String getDefaultFormatPattern(double min, double max) {
String format = null;
// calculate the default decimal format
double mantissa = Math.abs(max - min);
if (Math.abs(mantissa) > 0.1)
format = "############.##";
else {
format = "##.##";
while (mantissa < 1) {
mantissa *= 10.0;
format += "#";
}
}
return format;
}
/**
* Updates tick label for normal scale.
*
* @param min
* @param max
* @param length
* scale tick length (without margin)
*/
private void updateTickLabelForLinearScale(double min, double max, int length) {
double gridStep = getGridStep(length, min, max);
gridStepInPixel = (int) (length * gridStep / (max - min));
updateTickLabelForLinearScale(min, max, length, gridStep);
}
/**
* Updates tick label for normal scale.
*
* @param min
* @param max
* @param length
* scale tick length (without margin)
* @param tickStep
* the tick step
*/
private void updateTickLabelForLinearScale(double min, double max, int length, double tickStep) {
boolean minBigger = max < min;
double firstPosition;
// make firstPosition as the right most of min based on tickStep
if (min % tickStep <= 0) {
firstPosition = min - min % tickStep;
} else {
firstPosition = min - min % tickStep + tickStep;
}
// the unit time starts from 1:00
if (scale.isDateEnabled()) {
double zeroOclock = firstPosition - 3600000;
if (min < zeroOclock) {
firstPosition = zeroOclock;
}
}
// add min
boolean minDateAdded = false;
if (min > firstPosition == minBigger) {
tickLabelValues.add(min);
String lblStr;
if (isShowMinLabel()) {
if (scale.isDateEnabled()) {
Date date = new Date((long) min);
lblStr = scale.format(date, true);
minDateAdded = true;
} else {
lblStr = scale.format(min);
}
} else
lblStr = "";
tickLabels.add(lblStr);
tickLabelPositions.add(scale.getMargin());
}
int i = 1;
for (double b = firstPosition; max >= min ? b < max : b > max; b = firstPosition + i++ * tickStep) {
if (scale.isDateEnabled()) {
Date date = new Date((long) b);
tickLabels.add(scale.format(date, b == firstPosition && !minDateAdded));
} else {
tickLabels.add(scale.format(b));
}
tickLabelValues.add(b);
int tickLabelPosition = (int) ((b - min) / (max - min) * length) + scale.getMargin();
// - LINE_WIDTH;
tickLabelPositions.add(tickLabelPosition);
}
// always add max
tickLabelValues.add(max);
String lblStr;
if (showMaxLabel) {
if (scale.isDateEnabled()) {
Date date = new Date((long) max);
lblStr = scale.format(date, true);
} else {
lblStr = scale.format(max);
}
} else
lblStr = "";
tickLabels.add(lblStr);
tickLabelPositions.add(scale.getMargin() + length);
// }
}
/**
* Updates tick label for log scale.
* @param min
* @param max
* @param length
* the length of scale
*/
private void updateTickLabelForLogScale(double min, double max, int length) {
if (min <= 0 || max <= 0)
throw new IllegalArgumentException("the range for log scale must be in positive range");
boolean minBigger = max < min;
double logMin = Math.log10(min);
int minLogDigit = (int) Math.ceil(logMin);
int maxLogDigit = (int) Math.ceil(Math.log10(max));
final BigDecimal minDec = BigDecimal.valueOf(min);
BigDecimal tickStep = pow(10, minLogDigit - 1);
BigDecimal firstPosition;
if (minDec.remainder(tickStep).doubleValue() <= 0) {
firstPosition = minDec.subtract(minDec.remainder(tickStep));
} else {
if (minBigger)
firstPosition = minDec.subtract(minDec.remainder(tickStep));
else
firstPosition = minDec.subtract(minDec.remainder(tickStep)).add(tickStep);
}
// add min
boolean minDateAdded = false;
if (minDec.compareTo(firstPosition) == (minBigger ? 1 : -1)) {
minDateAdded = addMinMaxTickInfo(min, length, true);
}
for (int i = minLogDigit; minBigger ? i >= maxLogDigit : i <= maxLogDigit; i += minBigger ? -1 : 1) {
// if the range is too big skip minor ticks
if (Math.abs(maxLogDigit - minLogDigit) > 20) {
BigDecimal v = pow(10, i);
if (v.doubleValue() > max)
break;
addTickInfo(v, max, logMin, length, i == minLogDigit, minDateAdded);
} else {
// must use BigDecimal because it involves equal comparison
for (BigDecimal j = firstPosition; minBigger ? j.doubleValue() >= pow(10, i - 1).doubleValue()
: j.doubleValue() <= pow(10, i).doubleValue(); j = minBigger ? j.subtract(tickStep)
: j.add(tickStep)) {
if (minBigger ? j.doubleValue() < max : j.doubleValue() > max) {
break;
}
addTickInfo(j, max, logMin, length, j == firstPosition, minDateAdded);
}
tickStep = minBigger ? tickStep.divide(pow(10, 1)) : tickStep.multiply(pow(10, 1));
firstPosition = minBigger ? pow(10, i - 1) : tickStep.add(pow(10, i));
}
}
// add max
if (minBigger ? max < tickLabelValues.get(tickLabelValues.size() - 1)
: max > tickLabelValues.get(tickLabelValues.size() - 1)) {
addMinMaxTickInfo(max, length, false);
}
}
/**
* Add the tick labels, positions and values to the corresponding List used
* to store them.
*
* @param d
* BigDecimal value
* @param max
* maximum value
* @param logMin
* value used to calculate tick label position
* @param length
* value used to calculate tick label position
* @param isFirstPosition
* needed for date label
* @param minDateAdded
* needed for date label
*/
private void addTickInfo(BigDecimal d, double max, double logMin, int length, boolean isFirstPosition,
boolean minDateAdded) {
if (scale.isDateEnabled()) {
Date date = new Date((long) d.doubleValue());
tickLabels.add(scale.format(date, isFirstPosition && !minDateAdded));
} else {
tickLabels.add(scale.format(d.doubleValue()));
}
int tickLabelPosition = (int) ((Math.log10(d.doubleValue()) - logMin) / (Math.log10(max) - logMin) * length)
+ scale.getMargin();
tickLabelPositions.add(tickLabelPosition);
tickLabelValues.add(d.doubleValue());
}
/**
* Add the tick labels, positions and values for the min and max case to the
* corresponding List used to store them.
*
* @param value
* @param length
* used for max position
* @param isMin
* if True, we add the min related info, otherwise the max
* related info
* @return minDateAdded false by default, true if min and date are
* enabled
*/
private boolean addMinMaxTickInfo(double value, int length, boolean isMin) {
boolean minDateAdded = false;
if (isMin) {
tickLabelValues.add(value);
BigDecimal minDec = BigDecimal.valueOf(value);
if (scale.isDateEnabled()) {
Date date = new Date((long) minDec.doubleValue());
tickLabels.add(scale.format(date, true));
minDateAdded = true;
} else {
tickLabels.add(scale.format(minDec.doubleValue()));
}
tickLabelPositions.add(scale.getMargin());
} else {
tickLabelValues.add(value);
if (scale.isDateEnabled()) {
Date date = new Date((long) value);
tickLabels.add(scale.format(date, true));
} else {
tickLabels.add(scale.format(value));
}
tickLabelPositions.add(scale.getMargin() + length);
}
return minDateAdded;
}
/**
* Gets max length of tick label.
*/
private void updateTickLabelMaxLengthAndHeight() {
int maxLength = 0;
int maxHeight = 0;
for (int i = 0; i < tickLabels.size(); i++) {
if (tickLabelVisibilities.size() > i && tickLabelVisibilities.get(i)) {
Dimension p = FigureUtilities.getTextExtents(tickLabels.get(i), scale.getFont());
if (tickLabels.get(0).startsWith(MINUS) && !tickLabels.get(i).startsWith(MINUS)) {
p.width += FigureUtilities.getTextExtents(MINUS, scale.getFont()).width;
}
if (p.width > maxLength) {
maxLength = p.width;
}
if (p.height > maxHeight) {
maxHeight = p.height;
}
}
}
tickLabelMaxLength = maxLength;
tickLabelMaxHeight = maxHeight;
}
@Override
public int getHeadMargin() {
final Range r = scale.getScaleRange();
final Dimension l = scale.getDimension(r.getLower());
final Dimension h = scale.getDimension(r.getUpper());
if (scale.isHorizontal()) {
return (int) Math.ceil(Math.max(l.width, h.width) / 2.0);
}
return (int) Math.ceil(Math.max(l.height, h.height) / 2.0);
}
@Override
public int getTailMargin() {
return getHeadMargin();
}
/**
* Updates the visibility of tick labels.
*/
private void updateTickVisibility() {
tickLabelVisibilities.clear();
if (tickLabelPositions.isEmpty())
return;
for (int i = 0; i < tickLabelPositions.size(); i++) {
tickLabelVisibilities.add(Boolean.TRUE);
}
// set the tick label visibility
int previousPosition = 0;
String previousLabel = null;
for (int i = 0; i < tickLabelPositions.size(); i++) {
// check if it has space to draw
boolean hasSpaceToDraw = true;
String currentLabel = tickLabels.get(i);
int currentPosition = tickLabelPositions.get(i);
if (i != 0) {
hasSpaceToDraw = hasSpaceToDraw(previousPosition, currentPosition, previousLabel, currentLabel);
}
// check if repeated
boolean isRepeatSameTickAndNotEnd = currentLabel.equals(previousLabel)
&& (i != 0 && i != tickLabelPositions.size() - 1);
// check if it is major tick label
boolean isMajorTickOrEnd = true;
if (scale.isLogScaleEnabled()) {
isMajorTickOrEnd = isMajorTick(tickLabelValues.get(i)) || i == 0 || i == tickLabelPositions.size() - 1;
}
if (!hasSpaceToDraw || isRepeatSameTickAndNotEnd || !isMajorTickOrEnd) {
tickLabelVisibilities.set(i, Boolean.FALSE);
} else {
previousPosition = currentPosition;
previousLabel = currentLabel;
}
}
}
@Override
public Range update(final double min, final double max, final int length) {
tickLabels.clear();
tickLabelValues.clear();
tickLabelPositions.clear();
if (scale.isLogScaleEnabled()) {
updateTickLabelForLogScale(min, max, length);
} else {
updateTickLabelForLinearScale(min, max, length);
}
updateTickVisibility();
updateTickLabelMaxLengthAndHeight();
return null;
}
}