package org.csstudio.sds.components.ui.internal.figureparts; import java.math.BigDecimal; import java.util.ArrayList; import java.util.Calendar; import java.util.Date; import org.csstudio.swt.xygraph.linearscale.AbstractScale.LabelSide; import org.eclipse.draw2d.Figure; import org.eclipse.draw2d.FigureUtilities; import org.eclipse.draw2d.Graphics; import org.eclipse.draw2d.geometry.Dimension; import org.eclipse.draw2d.geometry.Point; import org.eclipse.draw2d.geometry.Rectangle; /** * Round Scale tick labels. * @author Xihui Chen */ public class RoundScaleTickLabels extends Figure { /** 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 radians */ private ArrayList<Double> tickLabelPositions; /** the array of visibility state of tick label */ private ArrayList<Boolean> tickVisibilities; /** the array of tick label area*/ private ArrayList<Rectangle> tickLabelAreas; /** the maximum height of tick labels */ private int tickLabelMaxOutLength; private double gridStepInRadians; private RoundScale scale; /** * Constructor. * @param scale * the round scale has this tick labels. */ protected RoundScaleTickLabels(RoundScale scale) { this.scale = scale; tickLabelValues = new ArrayList<Double>(); tickLabels = new ArrayList<String>(); tickLabelPositions = new ArrayList<Double>(); tickVisibilities = new ArrayList<Boolean>(); tickLabelAreas = new ArrayList<Rectangle>(); setFont(scale.getFont()); setForegroundColor(scale.getForegroundColor()); } /** * Updates the tick labels. * * @param lengthInDegrees * scale length in degrees */ protected void update(double lengthInDegrees, int lengthInPixels) { tickLabelValues.clear(); tickLabels.clear(); tickLabelPositions.clear(); if (scale.isLogScaleEnabled()) { updateTickLabelForLogScale(lengthInDegrees); }else { updateTickLabelForLinearScale(lengthInDegrees, lengthInPixels); } updateTickLabelAreas(); updateTickLabelMaxOutLength(); updateTickVisibility(); } /** * Updates tick label for log scale. * * @param lengthInDegrees * scale length in degrees */ private void updateTickLabelForLogScale(double lengthInDegrees) { double min = scale.getRange().getLower(); double max = scale.getRange().getUpper(); if(min <= 0 || max <= 0) throw new IllegalArgumentException( "the range for log scale must be in positive range"); 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 { firstPosition = MIN.subtract(MIN.remainder(tickStep)).add(tickStep); } //add min if(MIN.compareTo(firstPosition) == -1 ) { tickLabelValues.add(min); if (scale.isDateEnabled()) { Date date = new Date((long) MIN.doubleValue()); tickLabels.add(scale.format(date)); } else { tickLabels.add(scale.format(MIN.doubleValue())); } tickLabelPositions.add(scale.getStartAngle()*Math.PI/180); } for (int i = digitMin; i <= digitMax; i++) { for (BigDecimal j = firstPosition; j.doubleValue() <= pow(10, i) .doubleValue(); j = j.add(tickStep)) { if (j.doubleValue() > max) { break; } if (scale.isDateEnabled()) { Date date = new Date((long) j.doubleValue()); tickLabels.add(scale.format(date)); } else { tickLabels.add(scale.format(j.doubleValue())); } tickLabelValues.add(j.doubleValue()); double tickLabelPosition = scale.getStartAngle() - ((Math.log10(j.doubleValue()) - Math .log10(min)) / (Math.log10(max) - Math.log10(min)) * lengthInDegrees); tickLabelPosition = tickLabelPosition * Math.PI/180; tickLabelPositions.add(tickLabelPosition); } tickStep = tickStep.multiply(pow(10, 1)); firstPosition = tickStep.add(pow(10, i)); } //add max if(max > tickLabelValues.get(tickLabelValues.size()-1)) { tickLabelValues.add(max); if (scale.isDateEnabled()) { Date date = new Date((long) max); tickLabels.add(scale.format(date)); } else { tickLabels.add(scale.format(max)); } tickLabelPositions.add(scale.getEndAngle()*Math.PI/180); } } /** * Updates tick label for normal scale. * * @param lengthInDegrees * scale length in degrees * @param lengthInPixels * scale length in pixels */ private void updateTickLabelForLinearScale(double lengthInDegrees, int lengthInPixels) { double min = scale.getRange().getLower(); double max = scale.getRange().getUpper(); BigDecimal tickStep = getGridStep(lengthInPixels, min, max); gridStepInRadians = lengthInDegrees*(Math.PI/180)*tickStep.doubleValue()/(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); } //add min if(MIN.compareTo(firstPosition) == -1 ) { tickLabelValues.add(min); if (scale.isDateEnabled()) { Date date = new Date((long) MIN.doubleValue()); tickLabels.add(scale.format(date)); } else { tickLabels.add(scale.format(MIN.doubleValue())); } tickLabelPositions.add(scale.getStartAngle()*Math.PI/180); } for (BigDecimal b = firstPosition; b.doubleValue() <= max; b = b .add(tickStep)) { if (scale.isDateEnabled()) { Date date = new Date((long) b.doubleValue()); tickLabels.add(scale.format(date)); } else { tickLabels.add(scale.format(b.doubleValue())); } tickLabelValues.add(b.doubleValue()); double tickLabelPosition = scale.getStartAngle() - ((b.doubleValue() - min) / (max - min) * lengthInDegrees); tickLabelPosition = tickLabelPosition * Math.PI/180; tickLabelPositions.add(tickLabelPosition); } //add max if(max > tickLabelValues.get(tickLabelValues.size()-1)) { tickLabelValues.add(max); if (scale.isDateEnabled()) { Date date = new Date((long) max); tickLabels.add(scale.format(date)); } else { tickLabels.add(scale.format(max)); } tickLabelPositions.add((scale.getStartAngle()- lengthInDegrees)*Math.PI/180); } } /** * Updates the the draw area of each label. */ private void updateTickLabelAreas() { int lableRadius; tickLabelAreas.clear(); for(int i=0; i<tickLabelPositions.size(); i++) { Dimension ls = FigureUtilities.getTextExtents(tickLabels.get(i), scale.getFont()); if(scale.getTickLabelSide() == LabelSide.Primary) lableRadius = (int) (scale.getRadius() + RoundScaleTickMarks.MAJOR_TICK_LENGTH + RoundScale.SPACE_BTW_MARK_LABEL + ls.width/2 * Math.abs(Math.cos(tickLabelPositions.get(i))) + ls.height/2 * Math.abs(Math.sin(tickLabelPositions.get(i)))); else lableRadius = (int) (scale.getRadius() - RoundScaleTickMarks.MAJOR_TICK_LENGTH - RoundScale.SPACE_BTW_MARK_LABEL - ls.width/2 * Math.abs(Math.cos(tickLabelPositions.get(i))) - ls.height/2 * Math.abs(Math.sin(tickLabelPositions.get(i)))); Point lp = new PolarPoint(lableRadius, tickLabelPositions.get(i)).toRelativePoint( scale.getBounds()); tickLabelAreas.add(new Rectangle(lp.x - ls.width/2, lp.y - ls.height/2, ls.width, ls.height)); } } /** * Updates the visibility of tick labels. */ private void updateTickVisibility() { // initialize the array of tick label visibility state tickVisibilities.clear(); for (int i = 0; i < tickLabelPositions.size(); i++) { tickVisibilities.add(Boolean.TRUE); } if (tickLabelPositions.size() == 0) { return; } // set the tick label visibility Rectangle previousArea = tickLabelAreas.get(0); String previousLabel = null; for (int i = 0; i < tickLabelPositions.size(); i++) { // check if there is enough space to draw tick label boolean hasSpaceToDraw = true; if (i != 0) { if(i != (tickLabelPositions.size()-1)) hasSpaceToDraw = hasSpaceToDraw(previousArea, tickLabelAreas.get(i)) && hasSpaceToDraw(tickLabelAreas.get(i), tickLabelAreas.get(tickLabelPositions.size()-1)); else hasSpaceToDraw = hasSpaceToDraw(previousArea, tickLabelAreas.get(i)) && hasSpaceToDraw(tickLabelAreas.get(0), tickLabelAreas.get(i)); } // check if the same tick label is repeated String currentLabel = tickLabels.get(i); boolean isRepeatSameTickAndNotEnd = currentLabel.equals(previousLabel)&& (i!=0 && i!=tickLabelPositions.size()-1); previousLabel = currentLabel; // check if the tick label value is major boolean isMajorTickOrEnd = true; if (scale.isLogScaleEnabled()) { isMajorTickOrEnd = isMajorTick(tickLabelValues.get(i)) || i==0 || i==tickLabelPositions.size()-1; } if (!hasSpaceToDraw || isRepeatSameTickAndNotEnd || !isMajorTickOrEnd) { tickVisibilities.set(i, Boolean.FALSE); } else { previousArea = tickLabelAreas.get(i); } } } /** * 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 previousLabelArea * the previously drawn tick label area. * @param labelArea * the tick label's area * @return true if there is a space to draw tick label */ private boolean hasSpaceToDraw(Rectangle previousLabelArea, Rectangle labelArea) { return labelArea.getIntersection(previousLabelArea).isEmpty(); } /** * Gets max out length of tick label. */ private void updateTickLabelMaxOutLength() { int minLeft = 0; int maxRight =0; int minUp = 0; int maxDown =0; for(Rectangle rect : tickLabelAreas) { if (rect.x < minLeft) minLeft = rect.x; if(rect.x + rect.width > maxRight) maxRight = rect.x + rect.width; if(rect.y < minUp ) minUp = rect.y; if(rect.y + rect.height > maxDown) maxDown = rect.y + rect.height; } tickLabelMaxOutLength = Math.max( Math.max(maxRight - scale.getBounds().width, -minLeft), Math.max(maxDown - scale.getBounds().height, -minUp)); } /** * 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; } if (min >= max) { if(max == min) max ++; else throw new IllegalArgumentException("min must be less than max."); } double length = Math.abs(max - min); double gridStepHint = length / lengthInPixels * scale.getMajorTickMarkStepHint(); if(scale.isDateEnabled()) { //by default, make the least step to be minutes long timeStep = 60000l; 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); } // gridStepHint --> mantissa * 10 ** exponent // e.g. 724.1 --> 7.241 * 10 ** 2 double mantissa = gridStepHint; int exponent = 0; if (mantissa < 1) { while (mantissa < 1) { mantissa *= 10.0; exponent--; } } else { while (mantissa >= 10) { mantissa /= 10.0; exponent++; } } // calculate the grid step with hint. BigDecimal gridStep; if (mantissa > 7.5) { // gridStep = 10.0 * 10 ** exponent gridStep = BigDecimal.TEN.multiply(pow(10, exponent)); } else if (mantissa > 3.5) { // gridStep = 5.0 * 10 ** exponent gridStep = new BigDecimal(new Double(5).toString()).multiply(pow( 10, exponent)); } else if (mantissa > 1.5) { // gridStep = 2.0 * 10 ** exponent gridStep = new BigDecimal(new Double(2).toString()).multiply(pow( 10, exponent)); } else { // gridStep = 1.0 * 10 ** exponent gridStep = pow(10, exponent); } return gridStep; } /** * Gets the tick label positions. * * @return the tick label positions */ public ArrayList<Double> getTickLabelPositions() { return tickLabelPositions; } @Override protected void paintClientArea(Graphics graphics) { graphics.translate(bounds.x, bounds.y); drawTickLabels(graphics); super.paintClientArea(graphics); } /** * Draw the tick labels. * * @param grahics * the graphics context */ private void drawTickLabels(Graphics graphics) { // draw tick labels graphics.setFont(scale.getFont()); for (int i = 0; i < tickLabelPositions.size(); i++) { if (tickVisibilities.get(i) == true) { String text = tickLabels.get(i); graphics.drawText(text, tickLabelAreas.get(i).x, tickLabelAreas.get(i).y); } } } /** * @return if the tick label is draw outside scale's bounds, this is the * max length of the outside part */ public int getTickLabelMaxOutLength() { return tickLabelMaxOutLength; } /** * @return the tickVisibilities */ public ArrayList<Boolean> getTickVisibilities() { return tickVisibilities; } /** * @return the gridStepInPixel */ public double getGridStepInRadians() { return gridStepInRadians; } }