/*
* Copyright 2012 Diamond Light Source Ltd.
*
* 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.csstudio.swt.xygraph.linearscale;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.List;
import org.eclipse.draw2d.geometry.Dimension;
public class LinearScaleTicks implements ITicksProvider {
/** the array of tick label vales */
private ArrayList<Double> values;
/** the array of tick label */
private ArrayList<String> labels;
/** the array of tick label positions in pixels */
private ArrayList<Integer> positions;
/** the array of visibility state of tick label */
private ArrayList<Boolean> visibilities;
/** the array of label positions in pixels */
private ArrayList<Integer> lPositions;
/** the maximum width of tick labels */
private int maxWidth;
/** the maximum height of tick labels */
private int maxHeight;
/** number of pixels between major ticks */
private int majorStepInPixel;
/** number of pixels between minor ticks */
private int minorStepInPixel;
/** number of minor ticks between two major ticks */
private int minorTicks;
/** the array of minor tick positions in pixels */
private ArrayList<Integer> minorPositions;
private IScaleProvider scale;
public LinearScaleTicks(IScaleProvider scale) {
this.scale = scale;
values = new ArrayList<Double>();
labels = new ArrayList<String>();
positions = new ArrayList<Integer>();
lPositions = new ArrayList<Integer>();
visibilities = new ArrayList<Boolean>();
minorPositions = new ArrayList<Integer>();
}
@Override
public List<Integer> getPositions() {
return positions;
}
@Override
public int getPosition(int index) {
return positions.get(index);
}
@Override
public double getValue(int index) {
return values.get(index);
}
@Override
public String getLabel(int index) {
return labels.get(index);
}
@Override
public int getLabelPosition(int index) {
return lPositions.get(index);
}
@Override
public boolean isVisible(int index) {
return visibilities.get(index);
}
@Override
public int getMajorCount() {
return labels.size();
}
@Override
public int getMinorCount() {
return minorPositions.size();
}
@Override
public int getMinorPosition(int index) {
return minorPositions.get(index);
}
@Override
public int getMaxWidth() {
return maxWidth;
}
@Override
public int getMaxHeight() {
return maxHeight;
}
@Override
public Range update(final double min, final double max, final int length) {
values.clear();
labels.clear();
positions.clear();
lPositions.clear();
visibilities.clear();
minorPositions.clear();
if (scale.isLogScaleEnabled()) {
updateTickLabelForLogScale(min, max, length);
}else {
updateTickLabelForLinearScale(min, max, length);
}
updateTickVisibility();
updateLabelPositionsAndMaxDimensions(length);
updateMinorTickParameters();
if (!scale.isLogScaleEnabled()) {
updateMinorTicks();
}
return null;
}
@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 log scale.
*
* @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;
// if (min >= max) {
// throw new IllegalArgumentException("min must be less than max.");
// }
int digitMin = (int) Math.ceil(Math.log10(min));
int digitMax = (int) Math.ceil(Math.log10(max));
final BigDecimal MIN = new BigDecimal(new Double(min).toString());
BigDecimal tickStep = pow(10, digitMin - 1);
BigDecimal firstPosition;
if (MIN.remainder(tickStep).doubleValue() <= 0) {
firstPosition = MIN.subtract(MIN.remainder(tickStep));
} else {
if(minBigger)
firstPosition = MIN.subtract(MIN.remainder(tickStep));
else
firstPosition = MIN.subtract(MIN.remainder(tickStep)).add(tickStep);
}
//add min
if(MIN.compareTo(firstPosition) == (minBigger? 1:-1) ) {
values.add(min);
labels.add(scale.format(MIN.doubleValue()));
positions.add(scale.getMargin());
}
for (int i = digitMin; minBigger? i>=digitMax : i <= digitMax; i+=minBigger?-1:1) {
if(Math.abs(digitMax - digitMin) > 20){//if the range is too big, skip minor ticks.
BigDecimal v = pow(10,i);
if(v.doubleValue() > max)
break;
labels.add(scale.format(v.doubleValue()));
values.add(v.doubleValue());
int tickLabelPosition = (int) ((Math.log10(v.doubleValue()) - Math
.log10(min))
/ (Math.log10(max) - Math.log10(min)) * length)
+ scale.getMargin();
positions.add(tickLabelPosition);
}else{
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;
}
labels.add(scale.format(j.doubleValue()));
values.add(j.doubleValue());
int tickLabelPosition = (int) ((Math.log10(j.doubleValue()) - Math
.log10(min))
/ (Math.log10(max) - Math.log10(min)) * length)
+ scale.getMargin();
positions.add(tickLabelPosition);
}
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 < values.get(values.size()-1)
: max > values.get(values.size()-1)) {
values.add(max);
labels.add(scale.format(max));
positions.add(scale.getMargin() + length);
}
}
@Override
public int getHeadMargin() {
final Range r = scale.getScaleRange();
final Dimension l = scale.calculateDimension(r.getLower());
final Dimension h = scale.calculateDimension(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 tick label for normal scale.
*
* @param length
* scale tick length (without margin)
*/
private void updateTickLabelForLinearScale(double min, double max, int length) {
BigDecimal gridStepBigDecimal = getGridStep(length, min, max);
majorStepInPixel = (int) (length * gridStepBigDecimal.doubleValue()/(max - min));
updateTickLabelForLinearScale(min, max, length, gridStepBigDecimal);
}
/**
* Updates tick label for normal scale.
*
* @param length
* scale tick length (without margin)
* @param tickStep
* the tick step
*/
private void updateTickLabelForLinearScale(double min, double max, int length, BigDecimal tickStep) {
boolean minBigger = max < min;
final BigDecimal MIN = new BigDecimal(new Double(min).toString());
BigDecimal firstPosition;
//make firstPosition as the right most of min based on tickStep
/* if (min % tickStep <= 0) */
if (MIN.remainder(tickStep).doubleValue() <= 0) {
/* firstPosition = min - min % tickStep */
firstPosition = MIN.subtract(MIN.remainder(tickStep));
} else {
/* firstPosition = min - min % tickStep + tickStep */
firstPosition = MIN.subtract(MIN.remainder(tickStep)).add(tickStep);
}
// the unit time starts from 1:00
if (scale.isDateEnabled()) {
BigDecimal zeroOclock = firstPosition.subtract(new BigDecimal(
new Double(3600000).toString()));
if (MIN.compareTo(zeroOclock) == -1) {
firstPosition = zeroOclock;
}
}
//add min
int r = minBigger? 1 : -1;
if(MIN.compareTo(firstPosition) == r ) {
values.add(min);
labels.add(scale.format(MIN.doubleValue()));
positions.add(scale.getMargin());
}
for (BigDecimal b = firstPosition; max >= min ? b.doubleValue() <= max : b.doubleValue() >= max; b = b
.add(tickStep)) {
labels.add(scale.format(b.doubleValue()));
values.add(b.doubleValue());
int tickLabelPosition = (int) ((b.doubleValue() - min)
/ (max - min) * length) + scale.getMargin();
//- LINE_WIDTH;
positions.add(tickLabelPosition);
}
//add max
if((minBigger ? max < values.get(values.size()-1) :
max > values.get(values.size()-1) )) {
values.add(max);
labels.add("");
positions.add(scale.getMargin() + length);
}
}
/**
* Updates the visibility of tick labels.
*/
private void updateTickVisibility() {
// initialize the array of tick label visibility state
visibilities.clear();
for (int i = 0; i < positions.size(); i++) {
visibilities.add(Boolean.TRUE);
}
if (positions.size() == 0) {
return;
}
// set the tick label visibility
int previousPosition = 0;
String previousLabel = null;
for (int i = 0; i < positions.size(); i++) {
// check if there is enough space to draw tick label
boolean hasSpaceToDraw = true;
if (i != 0) {
try {
hasSpaceToDraw = hasSpaceToDraw(previousPosition,
positions.get(i), previousLabel, labels.get(i));
} catch (java.lang.IndexOutOfBoundsException iobe) {
hasSpaceToDraw = false;
}
}
// check if the same tick label is repeated
String currentLabel = labels.get(i);
boolean isRepeatSameTickAndNotEnd = currentLabel.equals(previousLabel) &&
(i!=0 && i!=positions.size()-1);
// check if the tick label value is major
boolean isMajorTickOrEnd = true;
if (scale.isLogScaleEnabled()) {
isMajorTickOrEnd = isMajorTick(values.get(i))
|| i==0 || i==positions.size()-1;
}
if (!hasSpaceToDraw || isRepeatSameTickAndNotEnd || !isMajorTickOrEnd) {
try {
visibilities.set(i, Boolean.FALSE);
} catch (java.lang.IndexOutOfBoundsException iobe) {
// Ignored or causes SWT error.
}
} else {
previousPosition = positions.get(i);
previousLabel = currentLabel;
}
}
}
/**
* Checks if the tick label is major (...,0.01,0.1,1,10,100,...).
*
* @param tickValue
* the tick label value
* @return true if the tick label is major
*/
private boolean isMajorTick(double tickValue) {
if (!scale.isLogScaleEnabled()) {
return true;
}
if (Math.log10(tickValue) % 1 == 0) {
return true;
}
return false;
}
/**
* Returns the state indicating if there is a space to draw tick label.
*
* @param previousPosition
* the previously drawn tick label position.
* @param tickLabelPosition
* the tick label position.
* @param previousTickLabel
* the previous tick label.
* @param tickLabel
* the tick label text
* @return true if there is a space to draw tick label
*/
private boolean hasSpaceToDraw(int previousPosition, int tickLabelPosition,
String previousTickLabel, String tickLabel) {
if (!scale.isHorizontal()) return true;
Dimension tickLabelSize = scale.calculateDimension(tickLabel);
Dimension previousTickLabelSize = scale.calculateDimension(previousTickLabel);
int interval = tickLabelPosition - previousPosition;
int textLength = (int) (scale.isHorizontal() ? (tickLabelSize.width/2.0 + previousTickLabelSize.width/2.0)
: tickLabelSize.height);
boolean noLapOnPrevoius = interval > textLength;
boolean noLapOnEnd = true;
if(tickLabelPosition != positions.get(positions.size() - 1)){
Dimension endTickLabelSize = scale.calculateDimension(labels.get(labels.size()-1));
interval = positions.get(positions.size() - 1) - tickLabelPosition;
textLength = (int) (scale.isHorizontal() ? (tickLabelSize.width/2.0 + endTickLabelSize.width/2.0)
: tickLabelSize.height);
noLapOnEnd = interval > textLength;
}
return noLapOnPrevoius && noLapOnEnd;
}
/**
* Update positions and max dimensions of tick labels
*/
private void updateLabelPositionsAndMaxDimensions(int length) {
maxWidth = 0;
maxHeight = 0;
for (int i = 0; i < labels.size(); i++) {
if (visibilities.size() > i && visibilities.get(i) == true) {
final String text = labels.get(i);
final Dimension d = scale.calculateDimension(text);
if (labels.get(0).startsWith("-") && !text.startsWith("-")) {
d.width += scale.calculateDimension("-").width;
}
if (d.width > maxWidth) {
maxWidth = d.width;
}
if(d.height > maxHeight){
maxHeight = d.height;
}
}
}
if (scale.isHorizontal()) { // re-expand length (so labels can flow into margins)
length += maxWidth;
} else {
length += maxHeight;
}
for (int i = 0; i < labels.size(); i++) {
int p = positions.get(i);
if (visibilities.size() > i && visibilities.get(i) == true) {
final Dimension d = scale.calculateDimension(labels.get(i));
if (scale.isHorizontal()) {
p = (int) Math.ceil(p - d.width*0.5);
if (p < 0) {
p = 0;
} else if (p + d.width >= length) {
p = length - 1 - d.width;
}
} else {
p = (int) Math.ceil(length - p - d.height*0.5);
if (p < 0) {
p = 0;
} else if (p + d.height >= length) {
p = length - 1 - d.height;
}
}
}
lPositions.add(p);
}
}
private void updateMinorTickParameters() {
if (scale.isDateEnabled()) {
minorTicks = 6;
minorStepInPixel = (int) (majorStepInPixel / 6.0);
return;
}
if (majorStepInPixel / 5 >= scale.getMinorTickMarkStepHint()) {
minorTicks = 5;
minorStepInPixel = (int) (majorStepInPixel / 5.0);
return;
}
if (majorStepInPixel / 4 >= scale.getMinorTickMarkStepHint()) {
minorTicks = 4;
minorStepInPixel = (int) (majorStepInPixel / 4.0);
return;
}
minorTicks = 2;
minorStepInPixel = (int) (majorStepInPixel / 2.0);
}
/**
* Calculates the value of the first argument raised to the power of the
* second argument.
*
* @param base
* the base
* @param expornent
* the exponent
* @return the value <tt>a<sup>b</sup></tt> in <tt>BigDecimal</tt>
*/
private BigDecimal pow(double base, int expornent) {
BigDecimal value;
if (expornent > 0) {
value = new BigDecimal(new Double(base).toString()).pow(expornent);
} else {
value = BigDecimal.ONE.divide(new BigDecimal(new Double(base)
.toString()).pow(-expornent));
}
return value;
}
/**
* Gets the grid step.
*
* @param lengthInPixels
* scale length in pixels
* @param min
* minimum value
* @param max
* maximum value
* @return rounded value.
*/
private BigDecimal getGridStep(int lengthInPixels, double min, double max) {
if((int) scale.getMajorGridStep() != 0) {
return new BigDecimal(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;
}
// throw new IllegalArgumentException("min must be less than max.");
}
double length = Math.abs(max - min);
double majorTickMarkStepHint = scale.getMajorTickMarkStepHint();
if(majorTickMarkStepHint > lengthInPixels)
majorTickMarkStepHint = lengthInPixels;
// if(min > max)
// majorTickMarkStepHint = -majorTickMarkStepHint;
double gridStepHint = length / lengthInPixels
* majorTickMarkStepHint;
if(scale.isDateEnabled()) {
//by default, make the least step to be minutes
long timeStep;
if(max - min < 10000) // < 10 sec, step = 1 sec
timeStep = 1000l;
else if(max - min < 60000) // < 1 min, step = 10 sec
timeStep= 10000l;
else if (max -min < 43200000) // < 12 hour, step = 1 min
timeStep = 60000l;
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 new BigDecimal(temp);
}
double mantissa = gridStepHint;
int exponent = 0;
if (mantissa < 1) {
if(mantissa != 0)
while (mantissa < 1) {
mantissa *= 10.0;
exponent--;
}
} else {
while (mantissa >= 10) {
mantissa /= 10.0;
exponent++;
}
}
BigDecimal gridStep;
if (mantissa > 7.5) {
gridStep = BigDecimal.TEN.multiply(pow(10, exponent)); // 10.0 * 10 ** exponent
} else if (mantissa > 3.5) {
gridStep = new BigDecimal(new Double(5).toString()).multiply(pow( // 5.0 * 10 ** exponent
10, exponent));
} else if (mantissa > 1.5) {
gridStep = new BigDecimal(new Double(2).toString()).multiply(pow( // 2.0 * 10 ** exponent
10, exponent));
} else {
gridStep = pow(10, exponent); // 1.0 * 10 ** exponent
}
if(minBigger)
gridStep = gridStep.negate();
return gridStep;
}
private void updateMinorTicks() {
final int imax = getMajorCount();
double lp = positions.get(0);
double cp, dp, tp;
for (int i = 1; i < imax; i++) {
cp = positions.get(i);
dp = cp - lp;
// add the first minor ticks which is start from min value
if (i == 1 && dp < majorStepInPixel) {
tp = cp;
while ((tp - lp) > minorStepInPixel + 3) {
tp -= minorStepInPixel;
minorPositions.add((int) tp);
}
} // add the last minor ticks which is end to max value
else if (i == imax - 1 && dp < majorStepInPixel) {
tp = lp;
while ((getPosition(i) - tp) > minorStepInPixel + 3) {
tp += minorStepInPixel;
minorPositions.add((int) tp);
}
} else { // add regular minor ticks
for (int j = 0; j < minorTicks; j++) {
tp = lp + (dp * j) / minorTicks;
minorPositions.add((int) tp);
}
}
lp = cp;
}
}
}